#!/bin/bash
#
# Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.  Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# This code 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
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#

# Setup the environment fixpath assumes. Read from command line options if
# available, or extract values automatically from the environment if missing.
# This is robust, but slower.
function setup() {

  # Make regexp tests case insensitive
  shopt -s nocasematch
  # Prohibit msys2 from meddling with paths
  export MSYS2_ARG_CONV_EXCL="*"
  #  Make sure WSL gets a copy of the path
  export WSLENV=PATH/l

  while getopts "e:p:r:t:c:qmi" opt; do
    case "$opt" in
    e) PATHTOOL="$OPTARG" ;;
    p) DRIVEPREFIX="$OPTARG" ;;
    r) ENVROOT="$OPTARG" ;;
    t) WINTEMP="$OPTARG" ;;
    c) CMD="$OPTARG" ;;
    q) QUIET=true ;;
    m) MIXEDMODE=true ;;
    i) IGNOREFAILURES=true ;;
    ?)
      # optargs found argument error
      exit 2
      ;;
    esac
  done

  shift $((OPTIND-1))
  ACTION="$1"

  # Locate variables ourself if not giving from caller
  if [[ -z ${PATHTOOL+x} ]]; then
    PATHTOOL="$(type -p cygpath)"
    if [[ $PATHTOOL == "" ]]; then
      PATHTOOL="$(type -p wslpath)"
      if [[ $PATHTOOL == "" ]]; then
        if [[ $QUIET != true ]]; then
          echo fixpath: failure: Cannot locate cygpath or wslpath >&2
        fi
        exit 2
      fi
    fi
  fi

  if [[ -z ${DRIVEPREFIX+x} ]]; then
    winroot="$($PATHTOOL -u c:/)"
    DRIVEPREFIX="${winroot%/c/}"
  else
    if [[ $DRIVEPREFIX == "NONE" ]]; then
      DRIVEPREFIX=""
    fi
  fi

  if [[ -z ${ENVROOT+x} ]]; then
    unixroot="$($PATHTOOL -w / 2> /dev/null)"
    # Remove trailing backslash
    ENVROOT="${unixroot%\\}"
  elif [[ "$ENVROOT" == "[unavailable]" ]]; then
    ENVROOT=""
  fi

  if [[ -z ${CMD+x} ]]; then
    CMD="$DRIVEPREFIX/c/windows/system32/cmd.exe"
  fi

  if [[ -z ${WINTEMP+x} ]]; then
    wintemp_win="$($CMD /q /c echo %TEMP% 2>/dev/null | tr -d \\n\\r)"
    WINTEMP="$($PATHTOOL -u "$wintemp_win")"
  fi
}

# Cleanup handling
TEMPDIRS=""
trap "cleanup" EXIT
function cleanup() {
  if [[ "$TEMPDIRS" != "" ]]; then
    rm -rf $TEMPDIRS
  fi
}

# Import a single path
# Result: imported path returned in $result
function import_path() {
  path="$1"
  # Strip trailing and leading space
  path="${path#"${path%%[![:space:]]*}"}"
  path="${path%"${path##*[![:space:]]}"}"

  if [[ $path =~ ^.:[/\\].*$ ]] || [[ "$path" =~ ^"$ENVROOT"\\.*$ ]] ; then
    # We got a Windows path as input; use pathtool to convert to unix path
    path="$($PATHTOOL -u "$path")"
    # Path will now be absolute
  else
    # Make path absolute, and resolve embedded '..' in path
    dirpart="$(dirname "$path")"
    dirpart="$(cd "$dirpart" 2>&1 > /dev/null && pwd)"
    if [[ $? -ne 0 ]]; then
      if [[ $QUIET != true ]]; then
        echo fixpath: failure: Directory containing path "'"$path"'" does not exist >&2
      fi
      if [[ $IGNOREFAILURES != true ]]; then
        exit 1
      else
        path=""
      fi
    else
      basepart="$(basename "$path")"
      if [[ $dirpart == / ]]; then
        # Avoid double leading /
        dirpart=""
      fi
      if [[ $basepart == / ]]; then
        # Avoid trailing /
        basepart=""
      fi
      path="$dirpart/$basepart"
    fi
  fi

  if [[ "$path" != "" ]]; then
    # Store current unix path
    unixpath="$path"
    # If $unixpath does not exist, add .exe (needed on WSL)
    if [[ ! -e "$unixpath" ]]; then
      unixpath="$unixpath.exe"
    fi
    # Now turn it into a windows path
    winpath="$($PATHTOOL -w "$unixpath" 2>/dev/null)"
    if [[ $? -eq 0 && -e "$unixpath" ]]; then
      if [[ ! "$winpath" =~ ^"$ENVROOT"\\.*$ ]] ; then
        # If it is not in envroot, it's a generic windows path
        if [[ ! $winpath =~ ^[-_.:\\a-zA-Z0-9]*$ ]] ; then
          # Path has forbidden characters, rewrite as short name
          # This monster of a command uses the %~s support from cmd.exe to
          # reliably convert to short paths on all winenvs.
          shortpath="$($CMD /q /c for %I in \( "$winpath" \) do echo %~sI 2>/dev/null | tr -d \\n\\r)"
          unixpath="$($PATHTOOL -u "$shortpath")"
          # unixpath is based on short name
        fi
        # Make it lower case
        path="$(echo "$unixpath" | tr '[:upper:]' '[:lower:]')"
      fi
    else
      # On WSL1, PATHTOOL will fail for files in envroot. If the unix path
      # exists, we assume that $path is a valid unix path.

      if [[ ! -e $path ]]; then
        if [[ -e $path.exe ]]; then
          path="$path.exe"
        else
          if [[ $QUIET != true ]]; then
            echo fixpath: warning: Path "'"$path"'" does not exist >&2
          fi
          # This is not a fatal error, maybe the path will be created later on
        fi
      fi
    fi
  fi

  if [[ "$path" =~ " " ]]; then
    # Our conversion attempts failed. Perhaps the path did not exists, and thus
    # we could not convert it to short name.
    if [[ $QUIET != true ]]; then
      echo fixpath: failure: Path "'"$path"'" contains space >&2
    fi
    if [[ $IGNOREFAILURES != true ]]; then
      exit 1
    else
      path=""
    fi
  fi

  result="$path"
}

# Import a single path, or a pathlist in Windows style (i.e. ; separated)
# Incoming paths can be in Windows or unix style.
# Returns in $result a converted path or path list
function import_command_line() {
  imported=""

  old_ifs="$IFS"
  IFS=";"
  for arg in $1; do
    if ! [[ $arg =~ ^" "+$ ]]; then
      import_path "$arg"

      if [[ "$result" != "" && "$imported" = "" ]]; then
        imported="$result"
      else
        imported="$imported:$result"
      fi
    fi
  done
  IFS="$old_ifs"

  result="$imported"
}

# If argument seems to be colon separated path list, and all elements
# are possible to convert to paths, make a windows path list
# Return 0 if successful with converted path list in $result, or
# 1 if it was not a path list.
function convert_pathlist() {
  converted_list=""
  pathlist_args="$1"

  IFS=':' read -r -a arg_array <<< "$pathlist_args"
  for arg in "${arg_array[@]}"; do
    winpath=""
    # Start looking for drive prefix
    if [[ $arg =~ ^($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
      winpath="${BASH_REMATCH[2]}:${BASH_REMATCH[3]}"
      # Change slash to backslash (or vice versa if mixed mode)
      if [[ $MIXEDMODE != true ]]; then
        winpath="${winpath//'/'/'\'}"
      else
        winpath="${winpath//'\'/'/'}"
      fi
    elif [[ $arg =~ ^(/[-_.*a-zA-Z0-9]+(/[-_.*a-zA-Z0-9]+)+.*$) ]] ; then
      # This looks like a unix path, like /foo/bar
      pathmatch="${BASH_REMATCH[1]}"
      if [[ $ENVROOT == "" ]]; then
        if [[ $QUIET != true ]]; then
          echo fixpath: failure: Path "'"$pathmatch"'" cannot be converted to Windows path >&2
        fi
        exit 1
      fi
      winpath="$ENVROOT$pathmatch"
      # Change slash to backslash (or vice versa if mixed mode)
      if [[ $MIXEDMODE != true ]]; then
        winpath="${winpath//'/'/'\'}"
      else
        winpath="${winpath//'\'/'/'}"
      fi
    else
      # This does not look like a path, so assume this is not a proper pathlist.
      # Flag this to caller.
      result=""
      return 1
    fi

    if [[ "$converted_list" = "" ]]; then
      converted_list="$winpath"
    else
      converted_list="$converted_list;$winpath"
    fi
  done

  result="$converted_list"
  return 0
}

# The central conversion function. Convert a single argument, so that any
# contained paths are converted to Windows style paths. Result is returned
# in $result. If it is a path list, convert it as one.
function convert_path() {
  if [[ $1 =~ : ]]; then
    convert_pathlist "$1"
    if [[ $? -eq 0 ]]; then
      return 0
    fi
    # Not all elements was possible to convert to Windows paths, so we
    # presume it is not a pathlist. Continue using normal conversion.
  fi

  arg="$1"
  winpath=""
  # Start looking for drive prefix. Also allow /xxxx prefixes (typically options
  # for Visual Studio tools), and embedded file:// URIs.
  if [[ $arg =~ ^([^/]*|-[^:=]*[:=]|.*file://|/[a-zA-Z:]{1,3}:?)($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
    prefix="${BASH_REMATCH[1]}"
    winpath="${BASH_REMATCH[3]}:${BASH_REMATCH[4]}"

    # If the thing in its entirety points to an existing path, use that instead of thinking
    # we have a prefix. This can only happen if the top-level directory has a single-letter name.
    if [[ ${#prefix} -eq 2 && "${prefix:0:1}" == "/" ]]; then
      possiblepath="${BASH_REMATCH[1]}/${BASH_REMATCH[3]}${BASH_REMATCH[4]}"
      if [[ -e "$possiblepath" || -e "$(dirname $possiblepath)" || -e "$(echo $possiblepath | cut -d / -f 1-5)" ]] ; then
        prefix=
        drivepart="${possiblepath:1:1}"
        pathpart="${possiblepath:2}"
        winpath="$drivepart:$pathpart"
      fi
    fi

    # Change slash to backslash (or vice versa if mixed mode)
    if [[ $MIXEDMODE != true ]]; then
      winpath="${winpath//'/'/'\'}"
    else
      winpath="${winpath//'\'/'/'}"
    fi
  elif [[ $arg =~ ^([^/]*|-[^:=]*[:=]|(.*file://))(/([-_.+a-zA-Z0-9]+)(/[-_.+a-zA-Z0-9]+)+)(.*)?$ ]] ; then
    # This looks like a unix path, like /foo/bar. Also embedded file:// URIs.
    prefix="${BASH_REMATCH[1]}"
    pathmatch="${BASH_REMATCH[3]}"
    firstdir="${BASH_REMATCH[4]}"
    suffix="${BASH_REMATCH[6]}"

    # We only believe this is a path if the first part is an existing directory
    # and the prefix is not a subdirectory in the current working directory. Remove
    # any part leading up to a : or = in the prefix before checking.
    if [[ -d "/$firstdir" && ! -d "${prefix##*:}" && ! -d "${prefix##*=}" ]];  then
      if [[ $ENVROOT == "" ]]; then
        if [[ $QUIET != true ]]; then
          echo fixpath: failure: Path "'"$pathmatch"'" cannot be converted to Windows path >&2
        fi
        exit 1
      fi
      winpath="$ENVROOT$pathmatch"
      # Change slash to backslash (or vice versa if mixed mode)
      if [[ $MIXEDMODE != true ]]; then
        winpath="${winpath//'/'/'\'}"
      else
        winpath="${winpath//'\'/'/'}"
      fi
      winpath="$winpath$suffix"
    fi
  fi

  if [[ $winpath != "" ]]; then
    result="$prefix$winpath"
  else
    # Return the arg unchanged
    result="$arg"
  fi
}

# Treat $1 as name of a file containing paths. Convert those paths to Windows style,
# and output them to the file specified by $2.
# If the output file already exists, it is overwritten.
function convert_file() {
  infile="$1"
  outfile="$2"
  if [[ -e $outfile ]] ; then
    rm $outfile
  fi
  while read line; do
    convert_path "$line"
    echo "$result" >> $outfile
  done < $infile
}

# Treat $1 as name of a file containing paths. Convert those paths to Windows style,
# in a new temporary file, and return a string "@<temp file>" pointing to that
# new file.
function convert_at_file() {
  infile="$1"
  if [[ -e $infile ]] ; then
    tempdir=$(mktemp -dt fixpath.XXXXXX -p "$WINTEMP")
    TEMPDIRS="$TEMPDIRS $tempdir"

    while read line; do
      convert_path "$line"
      echo "$result" >> $tempdir/atfile
    done < $infile
    convert_path "$tempdir/atfile"
    result="@$result"
  else
    result="@$infile"
  fi
}

# Convert an entire command line, replacing all unix paths with Windows paths,
# and all unix-style path lists (colon separated) with Windows-style (semicolon
# separated).
function print_command_line() {
  converted_args=""
  for arg in "$@" ; do
    if [[ $arg =~ ^@(.*$) ]] ; then
      # This is an @-file with paths that need converting
      convert_at_file "${BASH_REMATCH[1]}"
    else
      convert_path "$arg"
    fi
    converted_args="$converted_args$result "
  done
  result="${converted_args% }"
}

# Check if the winenv will allow us to start a Windows program when we are
# standing in the current directory
function verify_current_dir() {
  arg="$PWD"
  if [[ $arg =~ ^($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
    return 0
  elif [[ $arg =~ ^(/[^/]+.*$) ]] ; then
    if [[ $ENVROOT == "" || $ENVROOT =~ ^\\\\.* ]]; then
      # This is a WSL1 or WSL2 environment
      return 1
    fi
    return 0
  fi
  # This should not happen
  return 1
}

# The core functionality of fixpath. Take the given command line, and convert
# it and execute it, so that all paths are converted to Windows style.
# The return code is the return code of the executed command.
function exec_command_line() {
  # Check that Windows can handle our current directory (only an issue for WSL)
  verify_current_dir

  if [[ $? -ne 0 ]]; then
    # WSL1 will just forcefully put us in C:\Windows\System32 if we execute this from
    # a unix directory. WSL2 will do the same, and print a warning. In both cases,
    # we prefer to take control.
    cd "$WINTEMP"
    if [[ $QUIET != true ]]; then
      echo fixpath: warning: Changing directory to $WINTEMP >&2
    fi
  fi

  collected_args=()
  command=""
  for arg in "$@" ; do
    if [[ $command == "" ]]; then
      # We have not yet located the command to run
      if [[ $arg =~ ^(.*)=(.*)$ ]]; then
        # It's a leading env variable assignment (FOO=bar)
        key="${BASH_REMATCH[1]}"
        arg="${BASH_REMATCH[2]}"
        convert_path "$arg"
        # Set the variable to the converted result
        export $key="$result"
        # While this is only needed on WSL, it does not hurt to do everywhere
        export WSLENV=$WSLENV:$key/w
      else
        # The actual command will be executed by bash, so don't convert it
        command="$arg"
      fi
    else
      # Now we are collecting arguments; they all need converting
      if [[ $arg =~ ^@(.*$) ]] ; then
        # This is an @-file with paths that need converting
        convert_at_file "${BASH_REMATCH[1]}"
      else
        convert_path "$arg"
      fi
      collected_args=("${collected_args[@]}" "$result")
    fi
  done

  # Now execute it
  if [[ -v DEBUG_FIXPATH ]]; then
    echo fixpath: debug: input: "$@" >&2
    echo fixpath: debug: output: "$command" "${collected_args[@]}" >&2
  fi

  if [[ ! -e "$command" ]]; then
    if [[ -e "$command.exe" ]]; then
      command="$command.exe"
    fi
  fi

  if [[ $ENVROOT != "" || ! -x /bin/grep ]]; then
    "$command" "${collected_args[@]}"
  else
    # For WSL1, automatically strip away warnings from WSLENV=PATH/l
    "$command" "${collected_args[@]}" 2> >(/bin/grep -v "ERROR: UtilTranslatePathList" 1>&2)
  fi
}

# Check that the input represents a path that is reachable from Windows
function verify_command_line() {
  arg="$1"
  if [[ $arg =~ ^($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
    return 0
  elif [[ $arg =~ ^(/[^/]+/[^/]+.*$) ]] ; then
    if [[ $ENVROOT != "" ]]; then
      return 0
    fi
  fi
  return 1
}

#### MAIN FUNCTION

setup "$@"
# Shift away the options processed in setup
shift $((OPTIND))

if [[ "$ACTION" == "import" ]] ; then
  import_command_line "$@"
  echo "$result"
elif [[ "$ACTION" == "print" ]] ; then
  print_command_line "$@"
  echo "$result"
elif [[ "$ACTION" == "convert" ]] ; then
  convert_file "$@"
elif [[ "$ACTION" == "exec" ]] ; then
  exec_command_line "$@"
  # Propagate exit code
  exit $?
elif [[ "$ACTION" == "verify" ]] ; then
  verify_command_line "$@"
  exit $?
else
  if [[ $QUIET != true ]]; then
    echo Unknown operation: "$ACTION" >&2
    echo Supported operations: import print exec verify >&2
  fi
  exit 2
fi
