1#!/bin/bash
2#
3# Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
4# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
5#
6# This code is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License version 2 only, as
8# published by the Free Software Foundation.  Oracle designates this
9# particular file as subject to the "Classpath" exception as provided
10# by Oracle in the LICENSE file that accompanied this code.
11#
12# This code is distributed in the hope that it will be useful, but WITHOUT
13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
15# version 2 for more details (a copy is included in the LICENSE file that
16# accompanied this code).
17#
18# You should have received a copy of the GNU General Public License version
19# 2 along with this work; if not, write to the Free Software Foundation,
20# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21#
22# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
23# or visit www.oracle.com if you need additional information or have any
24# questions.
25#
26
27# Setup the environment fixpath assumes. Read from command line options if
28# available, or extract values automatically from the environment if missing.
29# This is robust, but slower.
30function setup() {
31  while getopts "e:p:r:t:c:qmi" opt; do
32    case "$opt" in
33    e) PATHTOOL="$OPTARG" ;;
34    p) DRIVEPREFIX="$OPTARG" ;;
35    r) ENVROOT="$OPTARG" ;;
36    t) WINTEMP="$OPTARG" ;;
37    c) CMD="$OPTARG" ;;
38    q) QUIET=true ;;
39    m) MIXEDMODE=true ;;
40    i) IGNOREFAILURES=true ;;
41    ?)
42      # optargs found argument error
43      exit 2
44      ;;
45    esac
46  done
47
48  shift $((OPTIND-1))
49  ACTION="$1"
50
51  # Locate variables ourself if not giving from caller
52  if [[ -z ${PATHTOOL+x} ]]; then
53    PATHTOOL="$(type -p cygpath)"
54    if [[ $PATHTOOL == "" ]]; then
55      PATHTOOL="$(type -p wslpath)"
56      if [[ $PATHTOOL == "" ]]; then
57        if [[ $QUIET != true ]]; then
58          echo fixpath: failure: Cannot locate cygpath or wslpath >&2
59        fi
60        exit 2
61      fi
62    fi
63  fi
64
65  if [[ -z ${DRIVEPREFIX+x} ]]; then
66    winroot="$($PATHTOOL -u c:/)"
67    DRIVEPREFIX="${winroot%/c/}"
68  else
69    if [[ $DRIVEPREFIX == "NONE" ]]; then
70      DRIVEPREFIX=""
71    fi
72  fi
73
74  if [[ -z ${ENVROOT+x} ]]; then
75    unixroot="$($PATHTOOL -w / 2> /dev/null)"
76    # Remove trailing backslash
77    ENVROOT="${unixroot%\\}"
78  elif [[ "$ENVROOT" == "[unavailable]" ]]; then
79    ENVROOT=""
80  fi
81
82  if [[ -z ${CMD+x} ]]; then
83    CMD="$DRIVEPREFIX/c/windows/system32/cmd.exe"
84  fi
85
86  if [[ -z ${WINTEMP+x} ]]; then
87    wintemp_win="$($CMD /q /c echo %TEMP% 2>/dev/null | tr -d \\n\\r)"
88    WINTEMP="$($PATHTOOL -u "$wintemp_win")"
89  fi
90
91  # Make regexp tests case insensitive
92  shopt -s nocasematch
93  # Prohibit msys2 from meddling with paths
94  export MSYS2_ARG_CONV_EXCL="*"
95  #  Make sure WSL gets a copy of the path
96  export WSLENV=PATH/l
97}
98
99# Cleanup handling
100TEMPDIRS=""
101trap "cleanup" EXIT
102function cleanup() {
103  if [[ "$TEMPDIRS" != "" ]]; then
104    rm -rf $TEMPDIRS
105  fi
106}
107
108# Import a single path
109# Result: imported path returned in $result
110function import_path() {
111  path="$1"
112  # Strip trailing and leading space
113  path="${path#"${path%%[![:space:]]*}"}"
114  path="${path%"${path##*[![:space:]]}"}"
115
116  if [[ $path =~ ^.:[/\\].*$ ]] || [[ "$path" =~ ^"$ENVROOT"\\.*$ ]] ; then
117    # We got a Windows path as input; use pathtool to convert to unix path
118    path="$($PATHTOOL -u "$path")"
119    # Path will now be absolute
120  else
121    # Make path absolute, and resolve embedded '..' in path
122    dirpart="$(dirname "$path")"
123    dirpart="$(cd "$dirpart" 2>&1 > /dev/null && pwd)"
124    if [[ $? -ne 0 ]]; then
125      if [[ $QUIET != true ]]; then
126        echo fixpath: failure: Directory containing path "'"$path"'" does not exist >&2
127      fi
128      if [[ $IGNOREFAILURES != true ]]; then
129        exit 1
130      else
131        path=""
132      fi
133    else
134      basepart="$(basename "$path")"
135      if [[ $dirpart == / ]]; then
136        # Avoid double leading /
137        dirpart=""
138      fi
139      if [[ $basepart == / ]]; then
140        # Avoid trailing /
141        basepart=""
142      fi
143      path="$dirpart/$basepart"
144    fi
145  fi
146
147  if [[ "$path" != "" ]]; then
148    # Now turn it into a windows path
149    winpath="$($PATHTOOL -w "$path" 2>/dev/null)"
150    # If it fails, try again with an added .exe (needed on WSL)
151    if [[ $? -ne 0 ]]; then
152      winpath="$($PATHTOOL -w "$path.exe" 2>/dev/null)"
153    fi
154    if [[ $? -eq 0 ]]; then
155      if [[ ! "$winpath" =~ ^"$ENVROOT"\\.*$ ]] ; then
156        # If it is not in envroot, it's a generic windows path
157        if [[ ! $winpath =~ ^[-_.:\\a-zA-Z0-9]*$ ]] ; then
158          # Path has forbidden characters, rewrite as short name
159          # This monster of a command uses the %~s support from cmd.exe to
160          # reliably convert to short paths on all winenvs.
161          shortpath="$($CMD /q /c for %I in \( "$winpath" \) do echo %~sI 2>/dev/null | tr -d \\n\\r)"
162          path="$($PATHTOOL -u "$shortpath")"
163          # Path is now unix style, based on short name
164        fi
165        # Make it lower case
166        path="$(echo "$path" | tr [:upper:] [:lower:])"
167      fi
168    else
169      # On WSL1, PATHTOOL will fail for files in envroot. If the unix path
170      # exists, we assume that $path is a valid unix path.
171
172      if [[ ! -e $path ]]; then
173        if [[ -e $path.exe ]]; then
174          path="$path.exe"
175        else
176          if [[ $QUIET != true ]]; then
177            echo fixpath: warning: Path "'"$path"'" does not exist >&2
178          fi
179          # This is not a fatal error, maybe the path will be created later on
180        fi
181      fi
182    fi
183  fi
184
185  if [[ "$path" =~ " " ]]; then
186    # Our conversion attempts failed. Perhaps the path did not exists, and thus
187    # we could not convert it to short name.
188    if [[ $QUIET != true ]]; then
189      echo fixpath: failure: Path "'"$path"'" contains space >&2
190    fi
191    if [[ $IGNOREFAILURES != true ]]; then
192      exit 1
193    else
194      path=""
195    fi
196  fi
197
198  result="$path"
199}
200
201# Import a single path, or a pathlist in Windows style (i.e. ; separated)
202# Incoming paths can be in Windows or unix style.
203# Returns in $result a converted path or path list
204function import_command_line() {
205  imported=""
206
207  old_ifs="$IFS"
208  IFS=";"
209  for arg in $1; do
210    if ! [[ $arg =~ ^" "+$ ]]; then
211      import_path "$arg"
212
213      if [[ "$result" != "" && "$imported" = "" ]]; then
214        imported="$result"
215      else
216        imported="$imported:$result"
217      fi
218    fi
219  done
220  IFS="$old_ifs"
221
222  result="$imported"
223}
224
225# If argument seems to be colon separated path list, and all elements
226# are possible to convert to paths, make a windows path list
227# Return 0 if successful with converted path list in $result, or
228# 1 if it was not a path list.
229function convert_pathlist() {
230  converted_list=""
231  pathlist_args="$1"
232
233  IFS=':' read -r -a arg_array <<< "$pathlist_args"
234  for arg in "${arg_array[@]}"; do
235    winpath=""
236    # Start looking for drive prefix
237    if [[ $arg =~ ^($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
238      winpath="${BASH_REMATCH[2]}:${BASH_REMATCH[3]}"
239      # Change slash to backslash (or vice versa if mixed mode)
240      if [[ $MIXEDMODE != true ]]; then
241        winpath="${winpath//'/'/'\'}"
242      else
243        winpath="${winpath//'\'/'/'}"
244      fi
245    elif [[ $arg =~ ^(/[-_.*a-zA-Z0-9]+(/[-_.*a-zA-Z0-9]+)+.*$) ]] ; then
246      # This looks like a unix path, like /foo/bar
247      pathmatch="${BASH_REMATCH[1]}"
248      if [[ $ENVROOT == "" ]]; then
249        if [[ $QUIET != true ]]; then
250          echo fixpath: failure: Path "'"$pathmatch"'" cannot be converted to Windows path >&2
251        fi
252        exit 1
253      fi
254      winpath="$ENVROOT$pathmatch"
255      # Change slash to backslash (or vice versa if mixed mode)
256      if [[ $MIXEDMODE != true ]]; then
257        winpath="${winpath//'/'/'\'}"
258      else
259        winpath="${winpath//'\'/'/'}"
260      fi
261    else
262      # This does not look like a path, so assume this is not a proper pathlist.
263      # Flag this to caller.
264      result=""
265      return 1
266    fi
267
268    if [[ "$converted_list" = "" ]]; then
269      converted_list="$winpath"
270    else
271      converted_list="$converted_list;$winpath"
272    fi
273  done
274
275  result="$converted_list"
276  return 0
277}
278
279# The central conversion function. Convert a single argument, so that any
280# contained paths are converted to Windows style paths. Result is returned
281# in $result. If it is a path list, convert it as one.
282function convert_path() {
283  if [[ $1 =~ : ]]; then
284    convert_pathlist "$1"
285    if [[ $? -eq 0 ]]; then
286      return 0
287    fi
288    # Not all elements was possible to convert to Windows paths, so we
289    # presume it is not a pathlist. Continue using normal conversion.
290  fi
291
292  arg="$1"
293  winpath=""
294  # Start looking for drive prefix. Also allow /xxxx prefixes (typically options
295  # for Visual Studio tools), and embedded file:// URIs.
296  if [[ $arg =~ ^([^/]*|-[^:=]*[:=]|.*file://|/[a-zA-Z:]{1,3}:?)($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
297    prefix="${BASH_REMATCH[1]}"
298    winpath="${BASH_REMATCH[3]}:${BASH_REMATCH[4]}"
299    # Change slash to backslash (or vice versa if mixed mode)
300    if [[ $MIXEDMODE != true ]]; then
301      winpath="${winpath//'/'/'\'}"
302    else
303      winpath="${winpath//'\'/'/'}"
304    fi
305  elif [[ $arg =~ ^([^/]*|-[^:=]*[:=]|(.*file://))(/([-_.+a-zA-Z0-9]+)(/[-_.+a-zA-Z0-9]+)+)(.*)?$ ]] ; then
306    # This looks like a unix path, like /foo/bar. Also embedded file:// URIs.
307    prefix="${BASH_REMATCH[1]}"
308    pathmatch="${BASH_REMATCH[3]}"
309    firstdir="${BASH_REMATCH[4]}"
310    suffix="${BASH_REMATCH[6]}"
311
312    # We only believe this is a path if the first part is an existing directory
313    if [[ -d "/$firstdir" ]];  then
314      if [[ $ENVROOT == "" ]]; then
315        if [[ $QUIET != true ]]; then
316          echo fixpath: failure: Path "'"$pathmatch"'" cannot be converted to Windows path >&2
317        fi
318        exit 1
319      fi
320      winpath="$ENVROOT$pathmatch"
321      # Change slash to backslash (or vice versa if mixed mode)
322      if [[ $MIXEDMODE != true ]]; then
323        winpath="${winpath//'/'/'\'}"
324      else
325        winpath="${winpath//'\'/'/'}"
326      fi
327      winpath="$winpath$suffix"
328    fi
329  fi
330
331  if [[ $winpath != "" ]]; then
332    result="$prefix$winpath"
333  else
334    # Return the arg unchanged
335    result="$arg"
336  fi
337}
338
339# Treat $1 as name of a file containg paths. Convert those paths to Windows style,
340# in a new temporary file, and return a string "@<temp file>" pointing to that
341# new file.
342function convert_at_file() {
343  infile="$1"
344  if [[ -e $infile ]] ; then
345    tempdir=$(mktemp -dt fixpath.XXXXXX -p "$WINTEMP")
346    TEMPDIRS="$TEMPDIRS $tempdir"
347
348    while read line; do
349      convert_path "$line"
350      echo "$result" >> $tempdir/atfile
351    done < $infile
352    convert_path "$tempdir/atfile"
353    result="@$result"
354  else
355    result="@$infile"
356  fi
357}
358
359# Convert an entire command line, replacing all unix paths with Windows paths,
360# and all unix-style path lists (colon separated) with Windows-style (semicolon
361# separated).
362function print_command_line() {
363  converted_args=""
364  for arg in "$@" ; do
365    if [[ $arg =~ ^@(.*$) ]] ; then
366      # This is an @-file with paths that need converting
367      convert_at_file "${BASH_REMATCH[1]}"
368    else
369      convert_path "$arg"
370    fi
371    converted_args="$converted_args$result "
372  done
373  result="${converted_args% }"
374}
375
376# Check if the winenv will allow us to start a Windows program when we are
377# standing in the current directory
378function verify_current_dir() {
379  arg="$PWD"
380  if [[ $arg =~ ^($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
381    return 0
382  elif [[ $arg =~ ^(/[^/]+.*$) ]] ; then
383    if [[ $ENVROOT == "" || $ENVROOT =~ ^\\\\.* ]]; then
384      # This is a WSL1 or WSL2 environment
385      return 1
386    fi
387    return 0
388  fi
389  # This should not happen
390  return 1
391}
392
393# The core functionality of fixpath. Take the given command line, and convert
394# it and execute it, so that all paths are converted to Windows style.
395# The return code is the return code of the executed command.
396function exec_command_line() {
397  # Check that Windows can handle our current directory (only an issue for WSL)
398  verify_current_dir
399
400  if [[ $? -ne 0 ]]; then
401    # WSL1 will just forcefully put us in C:\Windows\System32 if we execute this from
402    # a unix directory. WSL2 will do the same, and print a warning. In both cases,
403    # we prefer to take control.
404    cd "$WINTEMP"
405    if [[ $QUIET != true ]]; then
406      echo fixpath: warning: Changing directory to $WINTEMP >&2
407    fi
408  fi
409
410  collected_args=()
411  command=""
412  for arg in "$@" ; do
413    if [[ $command == "" ]]; then
414      # We have not yet located the command to run
415      if [[ $arg =~ ^(.*)=(.*)$ ]]; then
416        # It's a leading env variable assignment (FOO=bar)
417        key="${BASH_REMATCH[1]}"
418        arg="${BASH_REMATCH[2]}"
419        convert_path "$arg"
420        # Set the variable to the converted result
421        export $key="$result"
422        # While this is only needed on WSL, it does not hurt to do everywhere
423        export WSLENV=$WSLENV:$key/w
424      else
425        # The actual command will be executed by bash, so don't convert it
426        command="$arg"
427      fi
428    else
429      # Now we are collecting arguments; they all need converting
430      if [[ $arg =~ ^@(.*$) ]] ; then
431        # This is an @-file with paths that need converting
432        convert_at_file "${BASH_REMATCH[1]}"
433      else
434        convert_path "$arg"
435      fi
436      collected_args=("${collected_args[@]}" "$result")
437    fi
438  done
439
440  # Now execute it
441  if [[ -v DEBUG_FIXPATH ]]; then
442    echo fixpath: debug: input: "$@" >&2
443    echo fixpath: debug: output: "$command" "${collected_args[@]}" >&2
444  fi
445
446  if [[ ! -e "$command" ]]; then
447    if [[ -e "$command.exe" ]]; then
448      command="$command.exe"
449    fi
450  fi
451
452  if [[ $ENVROOT != "" || ! -x /bin/grep ]]; then
453    "$command" "${collected_args[@]}"
454  else
455    # For WSL1, automatically strip away warnings from WSLENV=PATH/l
456    "$command" "${collected_args[@]}" 2> >(/bin/grep -v "ERROR: UtilTranslatePathList" 1>&2)
457  fi
458}
459
460# Check that the input represents a path that is reachable from Windows
461function verify_command_line() {
462  arg="$1"
463  if [[ $arg =~ ^($DRIVEPREFIX/)([a-z])(/[^/]+.*$) ]] ; then
464    return 0
465  elif [[ $arg =~ ^(/[^/]+/[^/]+.*$) ]] ; then
466    if [[ $ENVROOT != "" ]]; then
467      return 0
468    fi
469  fi
470  return 1
471}
472
473#### MAIN FUNCTION
474
475setup "$@"
476# Shift away the options processed in setup
477shift $((OPTIND))
478
479if [[ "$ACTION" == "import" ]] ; then
480  import_command_line "$@"
481  echo "$result"
482elif [[ "$ACTION" == "print" ]] ; then
483  print_command_line "$@"
484  echo "$result"
485elif [[ "$ACTION" == "exec" ]] ; then
486  exec_command_line "$@"
487  # Propagate exit code
488  exit $?
489elif [[ "$ACTION" == "verify" ]] ; then
490  verify_command_line "$@"
491  exit $?
492else
493  if [[ $QUIET != true ]]; then
494    echo Unknown operation: "$ACTION" >&2
495    echo Supported operations: import print exec verify >&2
496  fi
497  exit 2
498fi
499