From 315f06f569ba704086d0660d7f6786eb35a5dac2 Mon Sep 17 00:00:00 2001 From: Karlson2k Date: Tue, 15 Mar 2016 23:07:19 +0300 Subject: tests: pathhelp.pm to process paths on Msys/Cygwin --- tests/pathhelp.pm | 761 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 tests/pathhelp.pm (limited to 'tests/pathhelp.pm') diff --git a/tests/pathhelp.pm b/tests/pathhelp.pm new file mode 100644 index 000000000..391ef6c25 --- /dev/null +++ b/tests/pathhelp.pm @@ -0,0 +1,761 @@ +########################################################################### +# _ _ ____ _ +# Project ___| | | | _ \| | +# / __| | | | |_) | | +# | (__| |_| | _ <| |___ +# \___|\___/|_| \_\_____| +# +# Copyright (C) 2016, Evgeny Grin (Karlson2k), . +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at https://curl.haxx.se/docs/copyright.html. +# +# You may opt to use, copy, modify, merge, publish, distribute and/or sell +# copies of the Software, and permit persons to whom the Software is +# furnished to do so, under the terms of the COPYING file. +# +# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +# KIND, either express or implied. +# +########################################################################### + +# This Perl package helps with path transforming when running cURL tests on +# Win32 platform with Msys or Cygwin. +# Three main functions 'sys_native_abs_path', 'sys_native_path' and +# 'build_sys_abs_path' autodetect format of given pathnames. Following formats +# are supported: +# (1) /some/path - absolute path in Unix-style +# (2) D:/some/path - absolute path in Win32-style +# (3) some/path - relative path +# (4) D:some/path - path relative to current directory on Win32 drive (paths +# like 'D:' are treated as 'D:./') (*) +# (5) \some/path - path from root directory on current Win32 drive (*) +# All forward '/' and back '\' slashes are treated identically except leading +# slash in forms (1) and (5). +# Forward slashes are simpler processed in Perl, do not require extra escaping +# for shell (unlike back slashes) and accepted by Win32 native programs, so +# all functions return paths with only forward slashes except +# 'sys_native_path' which returns paths with first forward slash for form (5). +# All returned paths don't contain any duplicated slashes, only single slashes +# are used as directory separators on output. +# On non-Windows platforms functions acts as transparent wrappers for similar +# Perl's functions or return unmodified string (depending on functionality), +# so all functions can be unconditionally used on all platforms. +# +# (*) CAUTION! Forms (4) and (5) are not recommended to use as they can be +# interpreted incorrectly in Perl and Msys/Cygwin environment have low +# control on Win32 current drive and Win32 current path on specific drive. + + +package pathhelp; +use strict; +use warnings; +use Cwd 'abs_path'; + +BEGIN { + require Exporter; + + our @ISA = qw(Exporter); + + our @EXPORT = qw( + sys_native_abs_path + sys_native_path + ); + + our @EXPORT_OK = qw( + build_sys_abs_path + sys_native_current_path + normalize_path + os_is_win + $use_cygpath + should_use_cygpath + drives_mounted_on_cygdrive + ); +} + + +####################################################################### +# Block for cached static variables +# +{ + # Cached static variable, Perl 5.0-compatible. + my $is_win = $^O eq 'MSWin32' + || $^O eq 'cygwin' + || $^O eq 'msys'; + + # Returns boolean true if OS is any form of Windows. + sub os_is_win { + return $is_win; + } + + # Cached static variable, Perl 5.0-compatible. + my $cygdrive_present; + + # Returns boolean true if Win32 drives mounted with '/cygdrive/' prefix. + sub drives_mounted_on_cygdrive { + return $cygdrive_present if defined $cygdrive_present; + $cygdrive_present = ((-e '/cygdrive/') && (-d '/cygdrive/')) ? 1 : 0; + return $cygdrive_present; + } +} + +our $use_cygpath; # Only for Win32: + # undef - autodetect + # 1 - use cygpath + # 0 - do not use cygpath + +# Returns boolean true if 'cygpath' utility should be used for path conversion. +sub should_use_cygpath { + unless (os_is_win()) { + $use_cygpath = 0; + return 0; + } + return $use_cygpath if defined $use_cygpath; + + $use_cygpath = (qx{cygpath -u '.\\' 2>/dev/null} eq "./\n" && $? == 0); + + return $use_cygpath; +} + +####################################################################### +# Performs path "normalization": all slashes converted to forward +# slashes (except leading slash), all duplicated slashes are replaced +# with single slashes, all relative directories ('./' and '../') are +# resolved if possible. +# Path processed as string, directories are not checked for presence so +# path for not yet existing directory can be "normalized". +# +sub normalize_path; + +####################################################################### +# Returns current working directory in Win32 format on Windows. +# +sub sys_native_current_path { + return Cwd::getcwd() unless os_is_win(); + + my $cur_dir; + if($^O eq 'msys') { + # MSys shell has built-in command. + chomp($cur_dir = `bash -c 'pwd -W'`); + if($? != 0) { + warn "Can't determine Win32 current directory.\n"; + return undef; + } + # Add final slash if required. + $cur_dir .= '/' if length($cur_dir) > 3; + } + else { + # Do not use 'cygpath' - it falsely succeed on paths like '/cygdrive'. + $cur_dir = `cmd "/c;" echo %__CD__%`; + if($? != 0 || substr($cur_dir, 0, 1) eq '%') { + warn "Can't determine Win32 current directory.\n"; + return undef; + } + # Remove both '\r' and '\n'. + $cur_dir =~ s{\n|\r}{}g; + + # Replace back slashes with forward slashes. + $cur_dir =~ s{\\}{/}g; + } + return $cur_dir; +} + +####################################################################### +# Returns Win32 current drive letter with colon. +# +sub get_win32_current_drive { + # Notice parameter "/c;" - it's required to turn off Msys's + # transformation of '/c' and compatible with Cygwin. + my $drive_letter = `cmd "/c;" echo %__CD__:~0,2%`; + if($? != 0 || substr($drive_letter, 1, 1) ne ':') { + warn "Can't determine current Win32 drive letter.\n"; + return undef; + } + + return substr($drive_letter, 0, 2); +} + +# Internal function. Converts path by using Msys's built-in transformation. +# Returned path may contain duplicated and back slashes. +sub do_msys_transform; + +# Internal function. Gets two parameters: first parameter must be single +# drive letter ('c'), second optional parameter is path relative to drive's +# current working directory. Returns Win32 absolute normalized path. +sub get_abs_path_on_win32_drive; + +# Internal function. Tries to find or guess Win32 version of given +# absolute Unix-style path. Other types of paths are not supported. +# Returned paths contain only single forward slashes (no back and +# duplicated slashes). +# Last resort. Used only when other transformations are not available. +sub do_dumb_guessed_transform; + +####################################################################### +# Converts given path to system native format, i.e. to Win32 format on +# Windows platform. Relative paths converted to relative, absolute +# paths converted to absolute. +# +sub sys_native_path { + my ($path) = @_; + + # Return untouched on non-Windows platforms. + return $path unless (os_is_win()); + + # Do not process empty path. + return $path if ($path eq ''); + + if($path =~ s{^([a-zA-Z]):$}{\u$1:}) { + # Path is single drive with colon. (C:) + # This type of paths is not processed correctly by 'cygpath'. + # WARNING! + # Be careful, this relative path can be accidentally transformed + # into wrong absolute path by adding to it some '/dirname' with + # slash at font. + return $path; + } + elsif($path =~ m{^\\} || $path =~ m{^[a-zA-Z]:[^/\\]}) { + # Path is a directory or filename on Win32 current drive or relative + # path on current directory on specific Win32 drive. + # ('\path' or 'D:path') + # First type of paths is not processed by Msys transformation and + # resolved to absolute path by 'cygpath'. + # Second type is not processed by Msys transformation and may be + # incorrectly processed by 'cygpath' (for paths like 'D:..\../.\') + + my $first_char = ucfirst(substr($path, 0, 1)); + + # Replace any back and duplicated slashes with single forward slashes. + $path =~ s{[\\/]+}{/}g; + + # Convert leading slash back to forward slash to indicate + # directory on Win32 current drive or capitalize drive letter. + substr($path, 0, 1) = $first_char; + return $path; + } + elsif(should_use_cygpath()) { + # 'cygpath' is available - use it. + + # Remove leading duplicated forward and back slashes, as they may + # prevent transforming and may be not processed. + $path =~ s{^([\\/])[\\/]+}{$1}g; + + my $has_final_slash = ($path =~ m{[/\\]$}); + + # Use 'cygpath', '-m' means Win32 path with forward slashes. + chomp($path = `cygpath -m '$path'`); + if ($? != 0) { + warn "Can't convert path by \"cygpath\".\n"; + return undef; + } + + # 'cygpath' may remove last slash for existing directories. + $path .= '/' if($has_final_slash); + + # Remove any duplicated forward slashes (added by 'cygpath' for root + # directories) + $path =~ s{//+}{/}g; + + return $path; + } + elsif($^O eq 'msys') { + # Msys transforms automatically path to Windows native form in staring + # program parameters if program is not Msys-based. + + $path = do_msys_transform($path); + return undef unless defined $path; + + # Capitalize drive letter for Win32 paths. + $path =~ s{^([a-z]:)}{\u$1}; + + # Replace any back and duplicated slashes with single forward slashes. + $path =~ s{[\\/]+}{/}g; + return $path; + } + elsif($path =~ s{^([a-zA-Z]):[/\\]}{\u$1:/}) { + # Path is already in Win32 form. ('C:\path') + + # Replace any back and duplicated slashes with single forward slashes. + $path =~ s{[\\/]+}{/}g; + return $path; + } + elsif($path !~ m{^/}) { + # Path is in relative form. ('path/name', './path' or '../path') + + # Replace any back and duplicated slashes with single forward slashes. + $path =~ s{[\\/]+}{/}g; + return $path; + } + + # OS is Windows, but not Msys, path is absolute, path is not in Win32 + # form and 'cygpath' is not available. + return do_dumb_guessed_transform($path); +} + +####################################################################### +# Converts given path to system native absolute path, i.e. to Win32 +# absolute format on Windows platform. Both relative and absolute +# formats are supported for input. +# +sub sys_native_abs_path { + my ($path) = @_; + + unless(os_is_win()) { + # Convert path to absolute form. + $path = Cwd::abs_path($path); + + # Do not process further on non-Windows platforms. + return $path; + } + + if($path =~ m{^([a-zA-Z]):($|[^/\\].*$)}) { + # Path is single drive with colon or relative path on Win32 drive. + # ('C:' or 'C:path') + # This kind of relative path is not processed correctly by 'cygpath'. + # Get specified drive letter + return get_abs_path_on_win32_drive($1, $2); + } + elsif($path eq '') { + # Path is empty string. Return current directory. + # Empty string processed correctly by 'cygpath'. + + return sys_native_current_path(); + } + elsif(should_use_cygpath()) { + # 'cygpath' is available - use it. + + my $has_final_slash = ($path =~ m{[\\/]$}); + + # Remove leading duplicated forward and back slashes, as they may + # prevent transforming and may be not processed. + $path =~ s{^([\\/])[\\/]+}{$1}g; + + print "Inter result: \"$path\"\n"; + # Use 'cygpath', '-m' means Win32 path with forward slashes, + # '-a' means absolute path + chomp($path = `cygpath -m -a '$path'`); + if($? != 0) { + warn "Can't resolve path by usung \"cygpath\".\n"; + return undef; + } + + # 'cygpath' may remove last slash for existing directories. + $path .= '/' if($has_final_slash); + + # Remove any duplicated forward slashes (added by 'cygpath' for root + # directories) + $path =~ s{//+}{/}g; + + return $path + } + elsif($path =~ s{^([a-zA-Z]):[/\\]}{\u$1:/}) { + # Path is already in Win32 form. ('C:\path') + + # Replace any possible back slashes with forward slashes, + # remove any duplicated slashes, resolve relative dirs. + return normalize_path($path); + } + elsif(substr($path, 0, 1) eq '\\' ) { + # Path is directory or filename on Win32 current drive. ('\Windows') + + my $w32drive = get_win32_current_drive(); + return undef unless defined $w32drive; + + # Combine drive and path. + # Replace any possible back slashes with forward slashes, + # remove any duplicated slashes, resolve relative dirs. + return normalize_path($w32drive . $path); + } + + unless (substr($path, 0, 1) eq '/') { + # Path is in relative form. Resolve relative directories in Unix form + # *BEFORE* converting to Win32 form otherwise paths like + # '../../../cygdrive/c/windows' will not be resolved. + my $cur_dir = `pwd -L`; + if($? != 0) { + warn "Can't determine current working directory.\n"; + return undef; + } + chomp($cur_dir); + + $path = $cur_dir . '/' . $path; + } + + # Resolve relative dirs. + $path = normalize_path($path); + return undef unless defined $path; + + if($^O eq 'msys') { + # Msys transforms automatically path to Windows native form in staring + # program parameters if program is not Msys-based. + $path = do_msys_transform($path); + return undef unless defined $path; + + # Replace any back and duplicated slashes with single forward slashes. + $path =~ s{[\\/]+}{/}g; + return $path; + } + # OS is Windows, but not Msys, path is absolute, path is not in Win32 + # form and 'cygpath' is not available. + + return do_dumb_guessed_transform($path); +} + +# Internal function. Converts given Unix-style absolute path to Win32 format. +sub simple_transform_win32_to_unix; + +####################################################################### +# Converts given path to build system format absolute path, i.e. to +# Msys/Cygwin Unix-style absolute format on Windows platform. Both +# relative and absolute formats are supported for input. +# +sub build_sys_abs_path { + my ($path) = @_; + + unless(os_is_win()) { + # Convert path to absolute form. + $path = Cwd::abs_path($path); + + # Do not process further on non-Windows platforms. + return $path; + } + + if($path =~ m{^([a-zA-Z]):($|[^/\\].*$)}) { + # Path is single drive with colon or relative path on Win32 drive. + # ('C:' or 'C:path') + # This kind of relative path is not processed correctly by 'cygpath'. + # Get specified drive letter + + # Resolve relative dirs in Win32-style path or paths like 'D:/../c/' + # will be resolved incorrectly. + # Replace any possible back slashes with forward slashes, + # remove any duplicated slashes. + $path = get_abs_path_on_win32_drive($1, $2); + return undef unless defined $path; + + return simple_transform_win32_to_unix($path); + } + elsif($path eq '') { + # Path is empty string. Return current directory. + # Empty string processed correctly by 'cygpath'. + + chomp($path = `pwd -L`); + if($? != 0) { + warn "Can't determine Unix-style current working directory.\n"; + return undef; + } + + # Add final slash if not at root dir. + $path .= '/' if length($path) > 2; + return $path; + } + elsif(should_use_cygpath()) { + # 'cygpath' is avalable - use it. + + my $has_final_slash = ($path =~ m{[\\/]$}); + + # Resolve relative directories, as they may be not resolved for + # Unix-style paths. + # Remove duplicated slashes, as they may be not processed. + $path = normalize_path($path); + return undef unless defined $path; + + # Use 'cygpath', '-u' means Unix-stile path, + # '-a' means absolute path + chomp($path = `cygpath -u -a '$path'`); + if($? != 0) { + warn "Can't resolve path by usung \"cygpath\".\n"; + return undef; + } + + # 'cygpath' removes last slash if path is root dir on Win32 drive. + # Restore it. + $path .= '/' if($has_final_slash && + substr($path, length($path) - 1, 1) ne '/'); + + return $path + } + elsif($path =~ m{^[a-zA-Z]:[/\\]}) { + # Path is already in Win32 form. ('C:\path') + + # Resolve relative dirs in Win32-style path otherwise paths + # like 'D:/../c/' will be resolved incorrectly. + # Replace any possible back slashes with forward slashes, + # remove any duplicated slashes. + $path = normalize_path($path); + return undef unless defined $path; + + return simple_transform_win32_to_unix($path); + } + elsif(substr($path, 0, 1) eq '\\') { + # Path is directory or filename on Win32 current drive. ('\Windows') + + my $w32drive = get_win32_current_drive(); + return undef unless defined $w32drive; + + # Combine drive and path. + # Resolve relative dirs in Win32-style path or paths like 'D:/../c/' + # will be resolved incorrectly. + # Replace any possible back slashes with forward slashes, + # remove any duplicated slashes. + $path = normalize_path($w32drive . $path); + return undef unless defined $path; + + return simple_transform_win32_to_unix($path); + } + + # Path is not in any Win32 form. + unless (substr($path, 0, 1) eq '/') { + # Path in relative form. Resolve relative directories in Unix form + # *BEFORE* converting to Win32 form otherwise paths like + # '../../../cygdrive/c/windows' will not be resolved. + my $cur_dir = `pwd -L`; + if($? != 0) { + warn "Can't determine current working directory.\n"; + return undef; + } + chomp($cur_dir); + + $path = $cur_dir . '/' . $path; + } + + return normalize_path($path); +} + +####################################################################### +# Performs path "normalization": all slashes converted to forward +# slashes (except leading slash), all duplicated slashes are replaced +# with single slashes, all relative directories ('./' and '../') are +# resolved if possible. +# Path processed as string, directories are not checked for presence so +# path for not yet existing directory can be "normalized". +# +sub normalize_path { + my ($path) = @_; + + # Don't process empty paths. + return $path if $path eq ''; + + unless($path =~ m{(?:^|\\|/)\.{1,2}(?:\\|/|$)}) { + # Speed up processing of simple paths. + my $first_char = substr($path, 0, 1); + $path =~ s{[\\/]+}{/}g; + # Restore starting backslash if any. + substr($path, 0, 1) = $first_char; + return $path; + } + + my @arr; + my $prefix; + my $have_root = 0; + + # Check whether path starts from Win32 drive. ('C:path' or 'C:\path') + if($path =~ m{^([a-zA-Z]:(/|\\)?)(.*$)}) { + $prefix = $1; + $have_root = 1 if defined $2; + # Process path separately from drive letter. + @arr = split(m{\/|\\}, $3); + # Replace backslash with forward slash if required. + substr($prefix, 2, 1) = '/' if $have_root; + } + else { + if($path =~ m{^(\/|\\)}) { + $have_root = 1; + $prefix = $1; + } + else { + $prefix = ''; + } + @arr = split(m{\/|\\}, $path); + } + + my $p = 0; + my @res; + + for my $el (@arr) { + if(length($el) == 0 || $el eq '.') { + next; + } + elsif($el eq '..' && @res > 0 && $res[$#res] ne '..') { + pop @res; + next; + } + push @res, $el; + } + if($have_root && @res > 0 && $res[0] eq '..') { + warn "Error processing path \"$path\": " . + "Parent directory of root directory does not exist!\n"; + return undef; + } + + my $ret = $prefix . join('/', @res); + $ret .= '/' if($path =~ m{\\$|/$} && scalar @res > 0); + + return $ret; +} + +# Internal function. Converts path by using Msys's built-in +# transformation. +sub do_msys_transform { + my ($path) = @_; + return undef if $^O ne 'msys'; + return $path if $path eq ''; + + # Remove leading double forward slashes, as they turn off Msys + # transforming. + $path =~ s{^/[/\\]+}{/}; + + # Msys transforms automatically path to Windows native form in staring + # program parameters if program is not Msys-based. + # Note: already checked that $path is non-empty. + $path = `cmd //c echo '$path'`; + if($? != 0) { + warn "Can't transform path into Win32 form by using Msys" . + "internal transformation.\n"; + return undef; + } + + # Remove double quotes, they are added for paths with spaces, + # remove both '\r' and '\n'. + $path =~ s{^\"|\"$|\"\r|\n|\r}{}g; + + return $path; +} + +# Internal function. Gets two parameters: first parameter must be single +# drive letter ('c'), second optional parameter is path relative to drive's +# current working directory. Returns Win32 absolute normalized path. +sub get_abs_path_on_win32_drive { + my ($drv, $rel_path) = @_; + my $res; + + # Get current directory on specified drive. + # "/c;" is compatible with both Msys and Cygwin. + my $cur_dir_on_drv = `cmd "/c;" echo %=$drv:%`; + if($? != 0) { + warn "Can't determine Win32 current directory on drive $drv:.\n"; + return undef; + } + + if($cur_dir_on_drv =~ m{^[%]}) { + # Current directory on drive is not set, default is + # root directory. + + $res = ucfirst($drv) . ':/'; + } + else { + # Current directory on drive was set. + # Remove both '\r' and '\n'. + $cur_dir_on_drv =~ s{\n|\r}{}g; + + # Append relative path part. + $res = $cur_dir_on_drv . '/'; + } + $res .= $rel_path if defined $rel_path; + + # Replace any possible back slashes with forward slashes, + # remove any duplicated slashes, resolve relative dirs. + return normalize_path($res); +} + +# Internal function. Tries to find or guess Win32 version of given +# absolute Unix-style path. Other types of paths are not supported. +# Returned paths contain only single forward slashes (no back and +# duplicated slashes). +# Last resort. Used only when other transformations are not available. +sub do_dumb_guessed_transform { + my ($path) = @_; + + # Replace any possible back slashes and duplicated forward slashes + # with single forward slashes. + $path =~ s{[/\\]+}{/}g; + + # Empty path is not valid. + return undef if (length($path) == 0); + + # RE to find Win32 drive letter + my $drv_ltr_re = drives_mounted_on_cygdrive() ? + qr{^/cygdrive/([a-zA-Z])($|/.*$)} : + qr{^/([a-zA-Z])($|/.*$)}; + + # Check path whether path is Win32 directly mapped drive and try to + # transform it assuming that drive letter is matched to Win32 drive letter. + if($path =~ m{$drv_ltr_re}) { + return ucfirst($1) . ':/' if(length($2) == 0); + return ucfirst($1) . ':' . $2; + } + + # This may be some custom mapped path. ('/mymount/path') + + # Must check longest possible path component as subdir can be mapped to + # different directory. For example '/usr/bin/' can be mapped to '/bin/' or + # '/bin/' can be mapped to '/usr/bin/'. + my $check_path = $path; + my $path_tail = ''; + do { + if(-d $check_path) { + my $res = + `(cd "$check_path" && cmd /c "echo %__CD__%") 2>/dev/null`; + if($? == 0 && substr($path, 0, 1) ne '%') { + # Remove both '\r' and '\n'. + $res =~ s{\n|\r}{}g; + + # Replace all back slashes with forward slashes. + $res =~ s{\\}{/}g; + + if(length($path_tail) > 0) { + return $res . $path_tail; + } + else { + $res =~ s{/$}{} unless $check_path =~ m{/$}; + return $res; + } + } + } + if($check_path =~ m{(^.*/)([^/]+/*)}) { + $check_path = $1; + $path_tail = $2 . $path_tail; + } + else { + # Shouldn't happens as root '/' directory should always + # be resolvable. + warn "Can't determine Win32 directory for path \"$path\".\n"; + return undef; + } + } while(1); +} + + +# Internal function. Converts given Unix-style absolute path to Win32 format. +sub simple_transform_win32_to_unix { + my ($path) = @_; + + if(should_use_cygpath()) { + # 'cygpath' gives precise result. + my $res; + chomp($res = `cygpath -a -u '$path'`); + if($? != 0) { + warn "Can't determine Unix-style directory for Win32 " . + "directory \"$path\".\n"; + return undef; + } + + # 'cygpath' removes last slash if path is root dir on Win32 drive. + $res .= '/' if(substr($res, length($res) - 1, 1) ne '/' && + $path =~ m{[/\\]$}); + return $res; + } + + # 'cygpath' is not available, use guessed transformation. + unless($path =~ s{^([a-zA-Z]):(?:/|\\)}{/\l$1/}) { + warn "Can't determine Unix-style directory for Win32 " . + "directory \"$path\".\n"; + return undef; + } + + $path = '/cygdrive' . $path if(drives_mounted_on_cygdrive()); + return $path; +} + +1; # End of module -- cgit v1.2.3