#!/bin/dash
#
# fstransform - transform a file-system to another file-system type,
#               preserving its contents and without the need for a backup
#
# Copyright (C) 2012 Massimiliano Ghilardi
# 
#     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 3 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, see <http://www.gnu.org/licenses/>.
#
# fstranform.sh
#
#  Created on: Jan 21, 2012
#      Author: max
#

____='           '
PROG=fstransform
PROG_VERSION=%PACKAGE_VERSION%

BOOT_CMD_which=which

CMDS_bootstrap="which expr id"
CMDS="stat mkfifo blockdev losetup fsck mkfs mount umount mkdir rmdir rm dd sync fsmove fsremap"
# optional commands
CMDS_optional="sleep date"
# commands that may need different variants for source and target file-systems
CMDS_dual="fsck"
# commands not found in environment
CMDS_missing=


# start with a clean environment
ERR=0
DEVICE=
FSTYPE=
FSTYPES_TESTED="minix ext2 ext3 ext4 reiserfs jfs xfs"
DEVICE_BLOCK_SIZE=
DEVICE_SIZE_IN_BYTES=
DEVICE_SIZE_IN_BLOCKS=
DEVICE_IS_INITIALLY_MOUNTED=
DEVICE_MOUNT_POINT=
DEVICE_FSTYPE=
LOOP_FILE=
LOOP_DEVICE=
LOOP_SIZE_IN_BYTES=
LOOP_MOUNT_POINT=
ZERO_FILE=

OPT_CREATE_ZERO_FILE=no
OPT_ASK_QUESTIONS=yes
OPT_TTY_SHOW_TIME=yes

OPTS_fsmove=
OPTS_fsremap=
OPTS_mkfs="-q"
OPTS_fsck_source="-p -f"
OPTS_fsck_target="-p -f"
X_COPY_LOOP_FILE=
X_COPY_DEVICE=

USER_ANSWER=
USER_LOOP_SIZE_IN_BYTES=
USER_FORCE_UNTESTED_FSTYPES=

for cmd in $CMDS_bootstrap $CMDS $CMDS_optional; do
  eval "CMD_$cmd="
done
for cmd in $CMDS_dual; do
  eval "CMD_${cmd}_source="
  eval "CMD_${cmd}_target="
done

TMP_DIR="/tmp"
VAR_TMP_DIR="/var/tmp"

FIFO_OUT="$TMP_DIR/fstransform.out.$$"
FIFO_ERR="$TMP_DIR/fstransform.err.$$"

PROG_DIR="$VAR_TMP_DIR/fstransform"
PROG_LOG_FILE="$VAR_TMP_DIR/fstransform/fstransform.log.$$"

# after log_init_file(), file descriptor 5 will be log file ~/.fstransform/fstransform.$$
exec 5>/dev/null

# after log_init_{tty,gui}(), file descriptor 1 will be tty or gui file descriptor


show_usage() {
  echo "Usage: $0 [OPTION]... DEVICE NEW-FILE-SYSTEM-TYPE"
  echo "Transform file system inside DEVICE to NEW-FILE-SYSTEM-TYPE,"
  echo "preserving its contents and without need for backup"
  echo
  echo "Options:"
  echo "  --help                        Print this help and exit"
  echo "  --new-size=SIZE               Set new file system length. default: device length"
  echo "  --old-file-system=OLD-TYPE    Override current (old) file system type autodetection"
  echo "  --force-untested-file-systems Transform also untested file systems (DANGEROUS)"
  echo "  --list-source-file-systems    Print list of file systems supported as source"
  echo "  --list-target-file-systems    Print list of file systems supported as target"
  echo "  --reversible[=yes|no]   Create zero-file, fsremap will do a reversible transformation"
  echo "                                default: no"
  echo "  --no-questions          Never ask any question or confirmation"
  echo "  --show-time[=yes|=no]   Show current time before each message. default: yes"
  echo "  --loop-file=LOOP-FILE   Override loop-file path"
  echo "  --loop-mount-point=PATH Override loop-file mount point"
  echo "  --zero-file=ZERO-FILE   Override zero-file path"
  echo "  --cmd-CMD-NAME=CMD-PATH Set external command CMD-NAME to use. default: autodetected"
  echo "  --opts-fsmove=OPTS      Pass OPTS as additional options to 'fsmove'"
  echo "  --opts-fsremap=OPTS     Pass OPTS as additional options to 'fsremap'"
  echo "  --opts-mkfs=OPTS        Pass OPTS as options to 'mkfs'. default: '-q'"
  echo "  --opts-fsck-source=OPTS Override 'fsck' options for old file system. default: '-p -f'"
  echo "  --opts-fsck-target=OPTS Override 'fsck' options for new file system. default: '-p -f'"
  echo "  --x-OPTION=VALUE        Set internal, undocumented option. For maintainers only."
}

show_tested_fstypes() {
  for fstype in $FSTYPES_TESTED; do
    echo "$fstype"
  done
}

if test "$#" = 1; then
  case "$1" in
    --help) show_usage; exit 0;;
    --list-source-file-systems) show_tested_fstypes; exit 0;;
    --list-target-file-systems) show_tested_fstypes; exit 0;;
  esac
fi


# parse command line arguments and set USER_CMD_* variables accordingly
parse_args() {
  log_info "parsing command line arguments"
  for arg in "$@"; do
    case "$arg" in
      --old-file-system=*)
        DEVICE_FSTYPE="`\"$CMD_expr\" match \"$arg\" '--old-fstype=\(.*\)'`"
	log_info "device old (initial) file-system type '$DEVICE_FSTYPE' specified on command line"
        ;;
      --new-size=*)
        USER_LOOP_SIZE_IN_BYTES="`\"$CMD_expr\" match \"$arg\" '--new-size=\(.*\)'`"
	log_info "device new (final) file-system length '$USER_LOOP_SIZE_IN_BYTES' bytes specified on command line"
        ;;
      --force-untested-file-systems)
        USER_FORCE_UNTESTED_FSTYPES="yes"
	log_info "forcing trasformation of untested file systems (DANGEROUS). '$arg' bytes specified on command line"
        ;;
      --reversible|--reversible=yes)
        OPT_CREATE_ZERO_FILE=yes
	log_info "zero file will be created, '$arg' specified on command line"
        ;;
      --reversible=no)
        OPT_CREATE_ZERO_FILE=no
	log_info "skipping creation of zero file, '$arg' specified on command line"
        ;;
      --no-questions)
        OPT_ASK_QUESTIONS=no
	log_info "assuming non-interactive execution, '$arg' specified on command line"
        ;;
      --show-time|--show-time=yes)
        OPT_TTY_SHOW_TIME=yes
	log_info "showing time-of-day for each message, '$arg' specified on command line"
        ;;
      --show-time=no)
        OPT_TTY_SHOW_TIME=no
	log_info "hiding time-of-day for each message, '$arg' specified on command line"
        ;;
      --loop-file=*)
        LOOP_FILE="`\"$CMD_expr\" match \"$arg\" '--loop-file=\(.*\)'`"
	log_info "loop file '$LOOP_FILE' specified on command line"
        ;;
      --loop-mount-point=*)
        LOOP_MOUNT_POINT="`\"$CMD_expr\" match \"$arg\" '--loop-mount-point=\(.*\)'`"
	log_info "loop file mount point '$LOOP_MOUNT_POINT' specified on command line"
        ;;
      --zero-file=*)
        ZERO_FILE="`\"$CMD_expr\" match \"$arg\" '--zero-file=\(.*\)'`"
	log_info "zero file '$ZERO_FILE' specified on command line"
        ;;
      --cmd-*=* )
        cmd="`\"$CMD_expr\" match \"$arg\" '--cmd-\(.*\)=.*'`"
        user_cmd="`\"$CMD_expr\" match \"$arg\" '--cmd-.*=\(.*\)'`"
        eval "USER_CMD_$cmd='$user_cmd'"
        ;;
      --opts-fsmove=*)
        OPTS_fsmove="`\"$CMD_expr\" match \"$arg\" '--opts-fsmove=\(.*\)'`"
	log_info "options '$OPTS_fsmove' for fsmove specified on command line"
	;;
      --opts-fsremap=*)
        OPTS_fsremap="`\"$CMD_expr\" match \"$arg\" '--opts-fsremap=\(.*\)'`"
	log_info "options '$OPTS_fsremap' for fsremap specified on command line"
	;;
      --opts-mkfs=*)
        OPTS_mkfs="`\"$CMD_expr\" match \"$arg\" '--opts-mkfs=\(.*\)'`"
	log_info "options '$OPTS_mkfs' for mkfs specified on command line"
	;;
      --opts-fsck-source=*)
        OPTS_fsck_source="`\"$CMD_expr\" match \"$arg\" '--opts-fsck-source=\(.*\)'`"
	log_info "options '$OPTS_fsck_source' for fsck(source file-system) specified on command line"
	;;
      --opts-fsck-target=*)
        OPTS_fsck_target="`\"$CMD_expr\" match \"$arg\" '--opts-fsck-target=\(.*\)'`"
	log_info "options '$OPTS_FSCK_TARGET_FS' for fsck(target file-system) specified on command line"
	;;
      --x-copy-device=*)
        X_COPY_DEVICE="`\"$CMD_expr\" match \"$arg\" '--x-copy-device=\(.*\)'`"
	log_info "(internal option) device will be copied to '$X_COPY_DEVICE' just before remapping"
        ;;
      --x-copy-loop-file=*)
        X_COPY_LOOP_FILE="`\"$CMD_expr\" match \"$arg\" '--x-copy-loop-file=\(.*\)'`"
	CMDS="$CMDS cmp"
	CMD_cmp=
	log_info "(internal option) loop file will be copied to '$X_COPY_LOOP_FILE'"
	log_info_add "command 'cmp' will be needed to verify it after transformation."
        ;;
      --*)
        log_info "ignoring unknown option '$arg'"
        ;;
      *)
        if test "$DEVICE" = ""; then
	  DEVICE="$arg"
	elif test "$FSTYPE" = ""; then
	  FSTYPE="$arg"
	else
          log_info "ignoring extra argument '$arg'"
	fi
	;;
    esac
  done
}




log_def_file() {
  log_file_timestamp() {
    if test "$CMD_date" != ""; then
      echo -n "`\"$CMD_date\" \"+%Y-%m-%d %H:%M:%S\"` " 1>&5
    fi
  }
  log_file_no_timestamp() {
    if test "$CMD_date" != ""; then
      echo -n "                   " 1>&5
    fi
  }
  log_file_info_4_cmd() {
    log_file_timestamp
    echo "$@" 1>&5
  }
  log_file_info() {
    log_file_timestamp
    echo "$PROG: $@" 1>&5
  }
  log_file_info_add() {
    log_file_no_timestamp
    echo "$____  $@" 1>&5
  }
  log_file_start() {
    log_file_timestamp
    echo -n "$PROG: $@" 1>&5
  }
  log_file_end() {
    log_file_timestamp
    echo "$@" 1>&5
  }
  log_file_warn() {
    log_file_no_timestamp
    echo 1>&5
    log_file_timestamp
    echo "$PROG: WARNING: $@" 1>&5
  }
  log_file_warn_add() {
    log_file_no_timestamp
    echo "$____  $@" 1>&5
  }
  log_file_warn_add_prompt() {
    log_file_no_timestamp
    echo "$____  $@" 1>&5
  }
  log_file_err() {
    log_file_no_timestamp
    echo 1>&5
    log_file_timestamp
    echo "ERROR! $PROG: $@" 1>&5  
  }
  log_file_err_add() {
    log_file_no_timestamp
    echo "       $@" 1>&5  
  }
  log_file_err_add_prompt() {
    log_file_no_timestamp
    echo "       $@" 1>&5  
  }
}

log_init_file() {
  "$CMD_mkdir" -p "$PROG_DIR" >/dev/null 2>&1
  > "$PROG_LOG_FILE" >/dev/null 2>&1
  
  if test -w "$PROG_LOG_FILE"; then
    exec 5>"$PROG_LOG_FILE"
  fi
}

log_def_tty() {
  read_user_answer() {
    if test "$OPT_ASK_QUESTIONS" = "yes"; then
      read USER_ANSWER
    else
      USER_ANSWER=
    fi
  }

  log_tty_timestamp() {
    if test "$OPT_TTY_SHOW_TIME" = "yes" -a "$CMD_date" != ""; then
      echo -n "`\"$CMD_date\" \"+%H:%M:%S\"` "
    fi
  }
  log_tty_no_timestamp() {
    if test "$OPT_TTY_SHOW_TIME" = "yes" -a "$CMD_date" != ""; then
      echo -n "         "
    fi
  }
  log_info_4_cmd() {
    log_tty_timestamp
    echo "$@"
    log_file_info_4_cmd "$@"
  }
  log_info() {
    log_tty_timestamp
    echo "$PROG: $@"
    log_file_info "$@"
  }
  log_info_add() {
    log_tty_no_timestamp
    echo "$____  $@"
    log_file_info_add "$@"
  }
  log_start() {
    log_tty_timestamp
    echo -n "$PROG: $@"
    log_file_start "$@"
  }
  log_end() {
    echo "$@"
    log_file_end "$@"
  }
  log_warn() {
    log_tty_no_timestamp
    echo
    log_tty_timestamp
    echo "$PROG: WARNING: $@"
    log_file_warn "$@"
  }
  log_warn_add() {
    log_tty_no_timestamp
    echo "$____  $@"
    log_file_warn_add "$@"
  }
  log_warn_add_prompt() {
    log_tty_no_timestamp
    echo -n "$____  $@"
    log_file_warn_add_prompt "$@"
  }
  log_err() {
    log_tty_no_timestamp
    echo
    log_tty_timestamp
    echo "ERROR! $PROG: $@"
    log_file_err "$@"
  }
  log_err_add() {
    log_tty_no_timestamp
    echo "       $@"
    log_file_err_add "$@"
  }
  log_err_add_prompt() {
    log_tty_no_timestamp
    echo -n "       $@"
    log_file_err_add_prompt "$@"
  }
}
log_init_tty() {
  log_quit() {
    # tty implementation of log_quit(): nothing to do
    return 0
  }
  return 0
}

log_quit() {
  # default implementation of log_quit(): nothing to do
  return 0
}

log_def_file
log_init_tty && log_def_tty

ERR="$?"
if test "$ERR" != 0; then
  echo "failed to initialize output to tty!"
  echo "exiting."
  exit "$ERR"
fi

log_info "starting version $PROG_VERSION, checking environment"

detect_cmd() {
  local my_cmd_which="$CMD_which"
  if test "$my_cmd_which" = ""; then
    my_cmd_which="$BOOT_CMD_which"
  fi

  local cmd="$1"
  local my_cmd=
  local user_cmd="`eval echo '$USER_CMD_'\"$cmd\"`"
  
  log_start "checking for $cmd... 	"
  
  if test "$user_cmd" != ""; then
    my_cmd="`$my_cmd_which \"$user_cmd\"`" >/dev/null 2>&1
    if test "$my_cmd" != ""; then
      if test -x "$my_cmd"; then
        log_end "'$my_cmd' ('$user_cmd' was specified)"
        eval "CMD_$cmd='$my_cmd'"
        return 0
      fi
    fi
  fi

  my_cmd="`$my_cmd_which \"$cmd\"`" >/dev/null 2>&1
  if test "$my_cmd" != ""; then
    if test -x "$my_cmd"; then
      log_end "'$my_cmd'"
      eval "CMD_$cmd='$my_cmd'"
      return 0
   else
      log_end "found '$my_cmd', but is NOT executable by you!"
    fi
  else
    log_end "NOT FOUND!"
  fi
  CMDS_missing="$CMDS_missing '$cmd'"
  return 1
}

detect_cmd_dual() {
  local my_cmd_which="$CMD_which"
  if test "$my_cmd_which" = ""; then
    my_cmd_which="$BOOT_CMD_which"
  fi
  
  local cmd="$1"
  local source_or_target="$2"
  
  local user_cmd="`eval echo '$USER_CMD_'\"$cmd\"'_'\"$source_or_target\"`"
  local my_cmd=
  log_start "checking for ${cmd}($source_or_target file-system)...	"

  if test "$user_cmd" != ""; then
    my_cmd="`$my_cmd_which \"$user_cmd\"`" >/dev/null 2>&1
    if test "$my_cmd" != ""; then
      if test -x "$my_cmd"; then
        log_end "'$my_cmd' ('$user_cmd' was specified)"
        eval "CMD_${cmd}_$source_or_target='$my_cmd'"
        return 0
      fi
    fi
  fi
  local nondual_cmd="`eval echo '$CMD_'\"$cmd\"`"
  log_end "'$nondual_cmd'"
  eval "CMD_${cmd}_$source_or_target='$nondual_cmd'"
  return 0
}

# apply fixes for special cases...
fix_for_special_cases() {
  if test "$DEVICE_FSTYPE" = "ntfs-3g"; then
    DEVICE_FSTYPE="ntfs"
  fi
  if test "$FSTYPE" = "ntfs-3g"; then
    FSTYPE="ntfs"
  fi
  local my_cmd_ntfsresize=
  if test "$FSTYPE" = "ntfs" -o "$DEVICE_FSTYPE" = "ntfs"; then
    log_info "applying special options for file-system type 'ntfs'"
    # we need 'ntfsresize', check if it's available
    my_cmd_ntfsresize="`$CMD_which ntfsresize`"
    if test "$my_cmd_ntfsresize" = ""; then
      log_warn "command 'ntfsresize' not found, it is needed to check integrity of file-system type 'ntfs'"
    fi
  fi
  
  if test "$DEVICE_FSTYPE" = "ntfs"; then
    if test "$my_cmd_ntfsresize" != "" -a "$USER_CMD_fsck_source" = ""; then
      USER_CMD_fsck_source="ntfsresize"
      OPTS_fsck_source="-n"
      detect_cmd_dual "fsck" "source"
    fi
  fi
  if test "$FSTYPE" = "ntfs"; then
    if test "$my_cmd_ntfsresize" != "" -a "$USER_CMD_fsck_target" = ""; then
      USER_CMD_fsck_target="ntfsresize"
      OPTS_fsck_target="-n"
      detect_cmd_dual "fsck" "target"
    fi
    # 'mkfs -t nfs' needs option '-f' (quick format) to maintain sparse files
    OPTS_mkfs="$OPTS_mkfs -f"
  fi
}

fail_missing_cmds() {
  log_err "environment check failed."
  log_err_add "Please install the commands$CMDS_missing before running $PROG"
  log_err_add "If these commands are already installed, add them to your \$PATH"
  log_err_add "or tell their location with the option --cmd-COMMAND=/path/to/your/command"
  exit "$ERR"
}





# bootstrap command detection (command 'which') and argument parsing (command 'expr')
for cmd in $CMDS_bootstrap; do
  detect_cmd "$cmd" || ERR="$?"
done
if test "$ERR" != 0; then
  fail_missing_cmds
fi

check_uid_0() {
  local my_uid="`$CMD_id -u`"
  if test "$my_uid" != 0; then
    log_err "this script must be executed as root (uid 0)"
    log_err_add "instead it is currently running as uid $my_uid"
    exit 1
  fi
}
check_uid_0


parse_args "$@"

for cmd in $CMDS; do
  detect_cmd "$cmd" || ERR="$?"
done

for cmd in $CMDS_dual; do
  detect_cmd_dual "$cmd" "source" || ERR="$?"
  detect_cmd_dual "$cmd" "target" || ERR="$?"
done

log_info "looking for optional commands"
for cmd in $CMDS_optional; do
  detect_cmd "$cmd"
done

fix_for_special_cases

if test "$ERR" != 0; then
  fail_missing_cmds
fi

log_info "environment check passed."

ERR=0

check_command_line_args() {
  local my_missing=
  if test "$DEVICE" != "" -a "$FSTYPE" != ""; then
    return 0
  elif test "$DEVICE" != ""; then
    my_missing="argument FSTYPE"
  elif test "$FSTYPE" != ""; then
    my_missing="argument DEVICE"
  else
    my_missing="arguments DEVICE and FSTYPE"
  fi
  log_err "missing command-line $my_missing."
  log_err "Try '$0 --help' for more information"
  exit 1
}
check_command_line_args


# inform if a command failed, and offer to fix manually
exec_cmd_status() {
  if test "$ERR" != 0; then
    log_err "command '$@' failed (exit status $ERR)"
    log_err_add "this is potentially a problem."
    if test "$OPT_ASK_QUESTIONS" = "yes"; then
      log_err_add "you can either quit now by pressing ENTER or CTRL+C,"
      log_err_add
      log_err_add "or, if you know what went wrong, you can fix it yourself,"
      log_err_add "then manually run the command '$@'"
      log_err_add "(or something equivalent)"
      log_err_add_prompt "and finally resume this script by typing CONTINUE and pressing ENTER: "
    else
      log_err_add
      log_err_add "you could try fix the problem yourself and continue"
      log_err_add "but this is a non-interactive run, so $PROG will exit now"
    fi
    read_user_answer
    if test "$USER_ANSWER" != "CONTINUE"; then
      log_info 'exiting.'
      exit "$ERR"
    fi
    ERR=0
  fi
}

# treat 'fsck' programs specially:
# they exit with status 1 to indicate that file-system errors were corrected
exec_cmd_status_fsck() {
  if test "$ERR" = 1; then
    log_info "command '$@' returned exit status 1"
    log_info_add "this means some file-system problems were found and corrected"
    log_info_add "on ext2, ext3 and ext4 it may simply mean harmless directory optimization"
    log_info_add "continuing..."
    ERR=0
  else
    exec_cmd_status "$@"
  fi
}


remove_fifo_out_err() {
  "$CMD_rm" -f "$FIFO_OUT" "$FIFO_ERR"
}

create_fifo_out_err() {
  remove_fifo_out_err
  "$CMD_mkfifo" -m 600 "$FIFO_OUT" "$FIFO_ERR"
  ERR="$?"
  exec_cmd_status "$CMD_mkfifo" -m 600 "$FIFO_OUT" "$FIFO_ERR"
}
create_fifo_out_err

CLEANUP_SIGNALS="HUP INT QUIT ILL TRAP ABRT BUS FPE KILL SEGV PIPE TERM URG XCPU XFSZ VTALRM PROF IO PWR SYS"
CLEANUP_0=
CLEANUP_1=
CLEANUP_2=
CLEANUP_3=
CLEANUP_4=

do_cleanup() {
  trap - 0 $CLEANUP_SIGNALS
  
  if test "$CLEANUP_4" != ""; then
    eval "$CLEANUP_4" >/dev/null 2>/dev/null
  fi
  if test "$CLEANUP_3" != ""; then
    eval "$CLEANUP_3" >/dev/null 2>/dev/null
  fi
  if test "$CLEANUP_2" != ""; then
    eval "$CLEANUP_2" >/dev/null 2>/dev/null
  fi
  if test "$CLEANUP_1" != ""; then
    eval "$CLEANUP_1" >/dev/null 2>/dev/null
  fi
  if test "$CLEANUP_0" != ""; then
    eval "$CLEANUP_0" >/dev/null 2>/dev/null
  fi
  remove_fifo_out_err
  log_quit
}
cleanup_on_exit() {
  do_cleanup
  log_quit
}
cleanup_on_signal() {
  do_cleanup
  exit 1
}
trap cleanup_on_exit 0
trap cleanup_on_signal $CLEANUP_SIGNALS




log_init_file
log_info "saving output of this execution into $PROG_LOG_FILE"







read_cmd_out_err() {
  local my_cmd_full="$1"
  local my_cmd="`\"$CMD_expr\" match \"$1\" '.*/\([^/]*\)'`"
  if test "$my_cmd" = ""; then
    my_cmd="$my_cmd_full"
  fi
  local my_fifo="$2"
  local my_prefix="$3"
  local my_out_1= my_out=
  while read my_out_1 my_out; do
    if test "$my_out_1" = "$my_cmd:"; then
      log_info_4_cmd "$my_prefix$my_cmd: $my_out"
    elif test "$my_out_1" = "$my_cmd_full:"; then
      log_info_4_cmd "$my_prefix$my_cmd: $my_out"
    else
      log_info_4_cmd "$my_prefix$my_cmd: $my_out_1 $my_out"
    fi
  done < "$my_fifo" &
}

read_cmd_out() {
  read_cmd_out_err "$1" "$FIFO_OUT" ""
}

read_cmd_err() {
  read_cmd_out_err "$1" "$FIFO_ERR" "warn: "
}

exec_cmd_noquit() {
  read_cmd_out "$1"
  "$@" >"$FIFO_OUT" 2>"$FIFO_OUT"
  ERR="$?"
  wait
}

exec_cmd() {
  exec_cmd_noquit "$@"
  exec_cmd_status "$@"
}

# treat 'fsck' programs specially:
# they exit with status 1 to indicate that file-system errors were corrected
exec_cmd_fsck() {
  exec_cmd_noquit "$@"
  exec_cmd_status_fsck "$@"
}

capture_cmd() {
  local my_ret my_var="$1"
  shift
  read_cmd_out "$1"
  my_ret="`\"$@\" 2>\"$FIFO_OUT\"`"
  ERR="$?"
  wait
  if test "$ERR" != 0; then
    log_err "command '$@' failed (exit status $ERR)"
    exit "$ERR"
  elif test "$my_ret" = ""; then
    log_err "command '$@' failed (no output)"
    exit 1
  fi
  eval "$my_var='$my_ret'"
}


check_device_is_block_special() {
  if test ! -b "$DEVICE"; then
    log_err "argument '$DEVICE' is NOT a block special device! Aborting."
    exit 1
  fi
}
check_device_is_block_special


log_info "preparing to transform device '$DEVICE' to file-system type '$FSTYPE'"


echo_device_mount_point_and_fstype() {
  local my_dev="$1"
  "$CMD_mount" | while read dev _on_ mount_point _type_ fstype opts; do
    if test "$dev" = "$my_dev"; then
      echo "$mount_point $fstype"
      break
    fi
  done
}

initial_mount_device() {
  DEVICE_MOUNT_POINT="$TMP_DIR/fstransform.mount.$$"
  "$CMD_mkdir" -p "$DEVICE_MOUNT_POINT" >/dev/null 2>&1
  
  CLEANUP_0="'$CMD_rmdir' '$DEVICE_MOUNT_POINT'"
  
  if test "$DEVICE_FSTYPE" != ""; then
    exec_cmd "$CMD_mount" -t "$DEVICE_FSTYPE" "$DEVICE" "$DEVICE_MOUNT_POINT"
  else
    exec_cmd "$CMD_mount" "$DEVICE" "$DEVICE_MOUNT_POINT"
  fi
  CLEANUP_0="'$CMD_umount' '$DEVICE'; $CLEANUP_0"
}

find_device_mount_point_and_fstype() {
  local my_dev="$DEVICE"
  local my_mount_point= my_fstype= my_now=
  local ret="`echo_device_mount_point_and_fstype \"$my_dev\"`"
  if test "$ret" = ""; then
    log_info "device '$my_dev' not found in the output of command $CMD_mount, assuming it is not mounted"
    initial_mount_device
    local ret="`echo_device_mount_point_and_fstype \"$my_dev\"`"
    if test "$ret" = ""; then
      log_err "just mounted device '$my_dev' at mount point '$DEVICE_MOUNT_POINT'"
      log_err_add "but still cannot find it in the output of command $CMD_mount"
      log_err_add "I give up, sorry"
      exit 1
    fi
    my_now=" now"
  else
    DEVICE_IS_INITIALLY_MOUNTED=yes
  fi
  for i in $ret; do
    if test "$my_mount_point" = ""; then
      my_mount_point="$i"
    else
      my_fstype="$i"
    fi
  done
  log_info "device is$my_now mounted at '$my_mount_point' with file-system type '$my_fstype'"
  if test ! -e "$my_mount_point"; then
    log_err "mount point '$my_mount_point' does not exist."
    log_err_add "maybe device '$my_dev' is mounted on a path containing spaces?"
    log_err_add "$PROG does not support mount points containing spaces in their path"
    exit 1
  fi
  if test ! -d "$my_mount_point"; then
    log_err "mount point '$my_mount_point' is not a directory"
    exit 1
  fi
  DEVICE_MOUNT_POINT="$my_mount_point"
  if test "$my_fstype" = fuseblk; then
    if test "$DEVICE_FSTYPE" != ""; then
      log_info "file-system type '$my_fstype' is a placeholder name for FUSE... ignoring it (user specified type '$DEVICE_FSTYPE')"
    else
      log_info "file-system type '$my_fstype' is a placeholder name for FUSE... ignoring it"
    fi
  elif test "$DEVICE_FSTYPE" != ""; then
    # let's compare user-specified file-system type with what we found...
    # still we honour what the user said.
    if test "$DEVICE_FSTYPE" != "$my_fstype"; then
      log_warn "does not match user-specified device file-system type '$DEVICE_FSTYPE'. using user-specified value."
    fi
  else
    DEVICE_FSTYPE="$my_fstype"
  fi
}
find_device_mount_point_and_fstype



check_for_tested_fstypes() {
  if test "$FSTYPE" = ""; then
    log_err "target file system type not specified!"
    exit 1
  fi
  local my_fstype_source_ok=no my_fstype_target_ok=no
  for my_fstype in $FSTYPES_TESTED; do
    if test "$DEVICE_FSTYPE" = "$my_fstype"; then
      my_fstype_source_ok=yes
      break
    fi
  done
  for my_fstype in $FSTYPES_TESTED; do
    if test "$FSTYPE" = "$my_fstype"; then
      my_fstype_target_ok=yes
      break
    fi
  done
  if test "$my_fstype_source_ok" = "yes" -a "$my_fstype_target_ok" = "yes"; then
    return 0
  fi
  
  local my_log=log_err
  if test "$USER_FORCE_UNTESTED_FSTYPES" = "yes"; then
    my_log=log_warn
  fi
  if test "$my_fstype_source_ok" != "yes"; then
    if test "$DEVICE_FSTYPE" = ""; then
      "$my_log" "failed to detect device current file system type"
    else
      "$my_log" "this program is UNTESTED on device current file system '$DEVICE_FSTYPE' !"
    fi
  fi
  if test "$my_fstype_target_ok" != "yes"; then
    "$my_log" "this program is UNTESTED on target file system '$FSTYPE' !"
  fi
  "$my_log" "this program is tested ONLY on file systems: $FSTYPES_TESTED"
  
  if test "$USER_FORCE_UNTESTED_FSTYPES" = "yes"; then
    "$my_log" "continuing anyway due to option '--force-untested-file-systems' (DANGEROUS)"
    return 0
  else
    "$my_log" "cowardly refusing to run. you can use option '--force-untested-file-systems' if you know what you are doing (DANGEROUS, you can LOSE your DATA)"
    exit 1
  fi
}
check_for_tested_fstypes



find_device_size() {
  capture_cmd DEVICE_SIZE_IN_BYTES "$CMD_blockdev" --getsize64 "$DEVICE"
  log_info "device raw size = $DEVICE_SIZE_IN_BYTES bytes"
}
find_device_size

create_loop_or_zero_file() {
  local my_kind="$1" my_var="$2" my_file="$3"
  local my_pattern="$DEVICE_MOUNT_POINT/.fstransform.$my_kind.*"
  local my_files="`echo $my_pattern`"
  if test "$my_files" != "$my_pattern"; then
    log_warn "possibly stale $PROG $my_kind files found inside device '$DEVICE',"
    log_warn_add "maybe they can be removed? list of files found:"
    log_warn_add
    log_warn_add "$my_files"
    log_warn_add
  fi
  if test "$my_file" = ""; then
    my_file="$DEVICE_MOUNT_POINT/.fstransform.$my_kind.$$"
    log_info "creating sparse $my_kind file '$my_file' inside device '$DEVICE'..."
    if test -e "$my_file"; then
      log_err "$my_kind file '$my_file' already exists! please remove it"
      exit 1
    fi
  else
    # check that user-specified file is actually inside DEVICE_MOUNT_POINT
    "$CMD_expr" match "$my_file" "$DEVICE_MOUNT_POINT/.*" >/dev/null 2>/dev/null || ERR="$?"
    if test "$ERR" != 0; then
      log_err "user-specified $my_kind file '$my_file' does not seem to be inside device mount point '$DEVICE_MOUNT_POINT'"
      log_err_add "please use a $my_kind file path that starts with '$DEVICE_MOUNT_POINT/'"
      exit "$ERR"
    fi
    "$CMD_expr" match "$my_file" '.*/\.\./.*' >/dev/null 2>/dev/null
    if test "$?" = 0; then
      log_err "user-specified $my_kind file '$my_var' contains '/../' in path"
      log_err_add "maybe somebody is trying to break $PROG?"
      log_err_add "I give up, sorry"
      exit "$ERR"
    fi
    log_info "overwriting $my_kind file '$my_file' inside device '$DEVICE'..."
  fi
  
  exec_cmd_noquit "$CMD_dd" "if=/dev/zero" "of=$my_file" bs=1 count=1
  if test "$ERR" != 0; then
    log_err "failed to create or truncate '$my_file' to zero bytes"
    log_err_add "maybe device '$DEVICE' is full or mounted read-only?"
    exit "$ERR"
  fi
  eval "$my_var='$my_file'"
}

create_loop_file() {
  create_loop_or_zero_file loop LOOP_FILE "$LOOP_FILE"
  
  CLEANUP_1="'$CMD_rm' -f '$LOOP_FILE'"

  # loop file length is = device size truncated down to a multiple of its block size:
  # avoids annoying problems if device's last block has an odd length
  
  capture_cmd DEVICE_BLOCK_SIZE "$CMD_stat" -c %o "$LOOP_FILE"
  log_info "device file-system block size = $DEVICE_BLOCK_SIZE bytes"
  if test "$DEVICE_BLOCK_SIZE" = "" -o "$DEVICE_BLOCK_SIZE" -lt 512; then
    # paranoia...
    DEVICE_BLOCK_SIZE=512
  fi
  : $(( DEVICE_SIZE_IN_BLOCKS = DEVICE_SIZE_IN_BYTES / DEVICE_BLOCK_SIZE ))
  : $(( LOOP_SIZE_IN_BYTES = DEVICE_SIZE_IN_BLOCKS * DEVICE_BLOCK_SIZE ))
  log_info "device usable size = $LOOP_SIZE_IN_BYTES bytes"
  
  if test "$USER_LOOP_SIZE_IN_BYTES" != ""; then
    # only accept user-specified new-size if smaller than maximum allowed,
    # and in any case truncate it down to a multiple of device block size
    if test "$USER_LOOP_SIZE_IN_BYTES" -lt "$LOOP_SIZE_IN_BYTES"; then
      : $(( LOOP_SIZE_IN_BYTES = USER_LOOP_SIZE_IN_BYTES / DEVICE_BLOCK_SIZE * DEVICE_BLOCK_SIZE ))
    fi
    log_info "sparse loop file will be $LOOP_SIZE_IN_BYTES bytes long (user specified $USER_LOOP_SIZE_IN_BYTES bytes)"
  fi
  exec_cmd "$CMD_dd" if=/dev/zero of="$LOOP_FILE" bs=1 count=1 seek="$(( LOOP_SIZE_IN_BYTES - 1 ))"
}
create_loop_file


connect_loop_device() {
  capture_cmd LOOP_DEVICE "$CMD_losetup" -f
  exec_cmd "$CMD_losetup" "$LOOP_DEVICE" "$LOOP_FILE"
  log_info "connected loop device '$LOOP_DEVICE' to file '$LOOP_FILE'"
  
  CLEANUP_2="'$CMD_losetup' -d '$LOOP_DEVICE'"
}
connect_loop_device

disconnect_loop_device() {
  local my_iter=0
  # loop device sometimes needs a little time to become free...
  for my_iter in 1 2 3 4; do
    exec_cmd "$CMD_sync"
    if test "$my_iter" != 1 -a "$CMD_sleep" != ""; then
      "$CMD_sleep" 5
      exec_cmd "$CMD_sync"
    fi
    if test "$my_iter" != 4; then
      "$CMD_losetup" -d "$LOOP_DEVICE" && break
    else
      exec_cmd "$CMD_losetup" -d "$LOOP_DEVICE"
    fi
  done
  log_info "disconnected loop device '$LOOP_DEVICE' from file '$LOOP_FILE'"

  CLEANUP_2=
}


format_loop_device() {
  log_info "formatting loop device '$LOOP_DEVICE' with file-system type '$FSTYPE'..."
  exec_cmd "$CMD_mkfs" -t "$FSTYPE" $OPTS_mkfs "$LOOP_DEVICE"
}
format_loop_device


mount_loop_file() {
  if test "$LOOP_MOUNT_POINT" = ""; then
    LOOP_MOUNT_POINT="$TMP_DIR/fstransform.loop.$$"
    exec_cmd "$CMD_mkdir" "$LOOP_MOUNT_POINT"
  else
    "$CMD_expr" match "$LOOP_MOUNT_POINT" "/.*" >/dev/null 2>/dev/null
    ERR="$?"
    if test "$ERR" != 0; then
      log_warn "user-specified loop file mount point '$LOOP_MOUNT_POINT' should start with '/'"
      log_warn_add "i.e. it should be an absolute path."
      log_warn_add "$PROG cannot ensure that '$LOOP_MOUNT_POINT' is outside '$DEVICE_MOUNT_POINT'"
      if test "$OPT_ASK_QUESTIONS" = "yes"; then
        log_warn_add "continue at your own risk"
        log_warn_add
        log_warn_add_prompt "press ENTER to continue, or CTRL+C to quit: "
        read_user_answer
      else
        log_err_add "you could examine the previous warning and decide to continue at your own risk"
        log_err_add "but this is a non-interactive run, so $PROG will exit now"
        log_info "exiting."
        exit "$ERR"
      fi
    else
      "$CMD_expr" match "$LOOP_MOUNT_POINT" "$DEVICE_MOUNT_POINT/.*" >/dev/null 2>/dev/null
      if test "$?" = 0; then
        log_err "user-specified loop file mount point '$LOOP_MOUNT_POINT' seems to be inside '$DEVICE_MOUNT_POINT'"
	log_err_add "maybe somebody is trying to break $PROG and lose data?"
	log_err_add "I give up, sorry"
	exit 1
      fi
    fi
  fi
  log_info "mounting loop device '$LOOP_DEVICE' on '$LOOP_MOUNT_POINT' ..."
  exec_cmd "$CMD_mount" -t "$FSTYPE" "$LOOP_DEVICE" "$LOOP_MOUNT_POINT"
  log_info "loop device '$LOOP_DEVICE' mounted successfully."
  
  CLEANUP_3="'$CMD_umount' $LOOP_DEVICE"
}
mount_loop_file


move_device_contents_into_loop_file() {
  log_info "preliminary steps completed, now comes the delicate part:"
  log_info "$PROG will move '$DEVICE' contents into the loop file."
  
  log_warn "THIS IS IMPORTANT! if either the original device '$DEVICE'"
  log_warn_add "or the loop device '$LOOP_DEVICE' become FULL,"
  log_warn_add
  log_warn_add " YOU  WILL  LOSE  YOUR  DATA !"
  log_warn_add
  log_warn_add "$PROG checks for enough available space,"
  log_warn_add "in any case it is recommended to open another terminal, type"
  log_warn_add "  watch df $DEVICE $LOOP_DEVICE"
  log_warn_add "and check that both the original device '$DEVICE'"
  log_warn_add "and the loop device '$LOOP_DEVICE' are NOT becoming full."
  log_warn_add "if one of them is becoming full (or both),"
  log_warn_add "you MUST stop $PROG with CTRL+C or equivalent."
  log_warn_add
  if test "$OPT_ASK_QUESTIONS" = "yes"; then
    log_warn_add "this is your chance to quit."
    log_warn_add_prompt "press ENTER to continue, or CTRL+C to quit: "
    read_user_answer
  fi
  log_info "moving '$DEVICE' contents into the loop file."
  log_info "this may take a long time, please be patient..."
  
  # do not remove $LOOP_FILE anymore: in a moment it will contain users' data!
  CLEANUP_1=
  
  exec_cmd "$CMD_fsmove" $OPTS_fsmove -- "$DEVICE_MOUNT_POINT" "$LOOP_MOUNT_POINT" --exclude "$LOOP_FILE"
  
}
move_device_contents_into_loop_file

umount_and_fsck_loop_file() {
  log_info "unmounting and running '$CMD_fsck_target' (disk check) on loop file '$LOOP_FILE'"
  exec_cmd "$CMD_umount" "$LOOP_DEVICE"
  
  CLEANUP_3=

  # ignore errors if removing "$LOOP_MOUNT_POINT" fails
  "$CMD_rmdir" "$LOOP_MOUNT_POINT" >/dev/null 2>/dev/null
  exec_cmd_fsck "$CMD_fsck_target" $OPTS_fsck_target "$LOOP_DEVICE"
  exec_cmd "$CMD_sync"

  if test "$X_COPY_LOOP_FILE" != ""; then
    log_info "(internal option) copying loop file '$LOOP_FILE' to '$X_COPY_LOOP_FILE'"
    exec_cmd "$CMD_dd" bs=64k if="$LOOP_DEVICE" of="$X_COPY_LOOP_FILE"
  fi
}
umount_and_fsck_loop_file

disconnect_loop_device

create_zero_file() {
  if test "$OPT_CREATE_ZERO_FILE" != "yes"; then
    return 0
  fi
  create_loop_or_zero_file zero ZERO_FILE "$ZERO_FILE"
  
  CLEANUP_4="'$CMD_rm' -f '$ZERO_FILE'"
  
  log_info "filling '$ZERO_FILE' with zeroes until device '$DEVICE' is full"
  log_info_add "needed by '$CMD_fsremap' to locate unused space."
  log_info_add "this may take a while, please be patient..."

  # trying to fill a device until it fails with "no space left on device" is not very nice
  # and can probably cause file-system corruption if device happens to be a loop-mounted file
  # which contains non-synced data.
  # to be safe, we 'sync' BEFORE and AFTER filling the device
  exec_cmd "$CMD_sync"

  # next command will fail with "no space left on device".
  # this is normal and expected.
  "$CMD_dd" if=/dev/zero of="$ZERO_FILE" bs=64k >/dev/null 2>/dev/null
  
  exec_cmd "$CMD_sync"
  log_info "file full of zeroes created successfully"
}
create_zero_file

remount_device_ro_and_fsck() {
  #log_info "remounting device '$DEVICE' read-only"
  #exec_cmd "$CMD_mount" "$DEVICE" -o remount,ro
  #exec_cmd "$CMD_sync"

  # cannot safely perform disk check on a mounted device... it must be unmounted first!
  log_info "unmounting device '$DEVICE' before disk check"
  exec_cmd "$CMD_umount" "$DEVICE"
  log_info "running '$CMD_fsck_source' (disk check) on device '$DEVICE'"
  exec_cmd_fsck "$CMD_fsck_source" $OPTS_fsck_source "$DEVICE"
  exec_cmd "$CMD_sync"
  
  if test "$X_COPY_DEVICE" != ""; then
    log_info "(internal option) copying device '$DEVICE' to '$X_COPY_DEVICE'"
    exec_cmd "$CMD_dd" bs=64k if="$DEVICE" of="$X_COPY_DEVICE"
  fi
  
  log_info "mounting again device '$DEVICE' read-only"
  if test "$DEVICE_FSTYPE" != ""; then
    exec_cmd "$CMD_mount" -t "$DEVICE_FSTYPE" "$DEVICE" "$DEVICE_MOUNT_POINT" -o ro
  else
    exec_cmd "$CMD_mount" "$DEVICE" "$DEVICE_MOUNT_POINT" -o ro
  fi
}
remount_device_ro_and_fsck


remap_device_and_sync() {
  local my_OPTS_fsremap="$OPTS_fsremap"
  if test "$OPT_ASK_QUESTIONS" != "yes"; then
    my_OPTS_fsremap="$my_OPTS_fsremap --no-questions"
  fi
  
  log_info "launching '$CMD_fsremap' in simulated mode"
  if test "$OPT_CREATE_ZERO_FILE" = "yes"; then
    exec_cmd "$CMD_fsremap" -n -q $my_OPTS_fsremap -- "$DEVICE" "$LOOP_FILE" "$ZERO_FILE"
  else
    exec_cmd "$CMD_fsremap" -n -q $my_OPTS_fsremap -- "$DEVICE" "$LOOP_FILE"
  fi
  
  log_info "launching '$CMD_fsremap' in REAL mode to perform in-place remapping."
  
  # starting to remap device, $ZERO_FILE will not exist anymore
  CLEANUP_4=
  
  if test "$OPT_CREATE_ZERO_FILE" = "yes"; then
    exec_cmd "$CMD_fsremap" -q --cmd-umount="$CMD_umount" $my_OPTS_fsremap -- "$DEVICE" "$LOOP_FILE" "$ZERO_FILE"
  else
    exec_cmd "$CMD_fsremap" -q --cmd-umount="$CMD_umount" $my_OPTS_fsremap -- "$DEVICE" "$LOOP_FILE"
  fi
  
  exec_cmd "$CMD_sync"
  

  if test "$X_COPY_LOOP_FILE" != ""; then
    log_info "(internal option) comparing transformed device '$DEVICE' with previously saved loop file '$X_COPY_LOOP_FILE'"
    
    # loop file may be smaller than device...
    # more exactly its length will be = device length rounded down to device block size
    "$CMD_dd" if="$DEVICE" bs="$DEVICE_BLOCK_SIZE" count="$DEVICE_SIZE_IN_BLOCKS" | "$CMD_cmp" - "$X_COPY_LOOP_FILE" || exit 1
  fi
}
remap_device_and_sync

fsck_device() {
  log_info "running again '$CMD_fsck_target' (disk check) on device '$DEVICE'"
  exec_cmd_fsck "$CMD_fsck_target" $OPTS_fsck_target "$DEVICE"
}
fsck_device


final_mount_device() {
  log_info "mounting transformed device '$DEVICE'"
  exec_cmd "$CMD_mount" -t "$FSTYPE" "$DEVICE" "$DEVICE_MOUNT_POINT"
  log_info "completed successfully. your new '$FSTYPE' file-system is mounted at '$DEVICE_MOUNT_POINT'"
}

if test "$DEVICE_IS_INITIALLY_MOUNTED" = "yes"; then
  final_mount_device
else
  log_info "completed successfully. device '$DEVICE' now contains '$FSTYPE' file-system"
fi

