#
# Copyright (c) 1996, 1997, 1998 Shigio Yamaguchi. All rights reserved.
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#
#       File::PathConvert.pm
#

package File::PathConvert;
require 5.002;

use strict ;

BEGIN {
   use Exporter   ();
   use vars       qw($VERSION @ISA @EXPORT_OK);
   $VERSION       = 0.85;
   @ISA           = qw(Exporter);
   @EXPORT_OK     = qw(setfstype splitpath joinpath splitdirs joindirs realpath abs2rel rel2abs $maxsymlinks $verbose $SL $resolved );
}

use vars      qw( $maxsymlinks $verbose $SL $resolved ) ;
use Cwd;

#
# Initialize @EXPORT_OK vars
#
$maxsymlinks   = 32;       # allowed symlink number in a path
$verbose       = 0;        # 1: verbose on, 0: verbose off
$SL            = '' ;      # Separator char export
$resolved      = '' ;      # realpath() intermediate value export

#############################################################################
#
#  Package Globals
#

my $fstype        ; # A name indicating the type of filesystem currently in use
my $sep           ; # separator
my $sepRE         ; # RE to match spearator
my $notsepRE      ; # RE to match anything else
my $volumeRE      ; # RE to match the volume name
my $directoryRE   ; # RE to match the directory name
my $isrootRE      ; # RE to match root path: applied to directory portion only
my $thisDir       ; # Name of this directory
my $thisDirRE     ; # Name of this directory
my $parentDir     ; # Name of parent directory
my $parentDirRE   ; # RE to match parent dir name
my $casesensitive ; # Set to non-zero for case sensitive name comprisions.  Only
                    # affects names, not any other REs, so $isrootRE for Win32
                    # must be case insensitive
my $idempotent    ; # Set to non-zero if '//' is equivalent to '/'.  This
                    # does not affect leading '//' and '\\' under Win32,
                    # but will fold '///' and '////', etc, in to '//' on this
                    # Win32



###########
#
# The following globals are regexs used in the indicated routines.  These
# are initialized by setfstype, so they don't need to be rebuilt each time
# the routine that uses them is called.

my $basenamesplitRE ; # Used in realpath() to split filenames.


###########
#
# This RE matches (and saves) the portion of the string that is just before
# the beginning of a name
#
my $beginning_of_name ;

#
# This whopper of an RE looks for the pattern "name/.." if it occurs
# after the beginning of the string or after the root RE, or after a separator.
# We don't assume that the isrootRE has a trailing separator.
# It also makes sure that we aren't eliminating '../..' and './..' patterns
# by using the negative lookahead assertion '(?!' ... ')' construct.  It also
# ignores 'name/..name'.
#
my $name_sep_parentRE ;

#
# Matches '..$', '../' after a root
my $leading_parentRE ;

#
# Matches things like '/(./)+' and '^(./)+'
#
my $dot_sep_etcRE ;

#
# Matches trailing '/' or '/.'
#
my $trailing_sepRE ;


#############################################################################
#
#     Functions
#


#
# setfstype: takes the name of an operating system and sets up globals that
#            allow the other functions to operate on multiple OSs.  See 
#            %fsconfig for the sets of settings.
#
#            This is run once on module load to configure for the OS named
#            in $^O.
#
# Interface:
#       i)     $osname, as in $^O or plain english: "MacOS", "DOS, etc.
#              This is _not_ usually case sensitive.
#       r)     Name of recognized name on success else undef.  Note that, as
#              shipped, 'unix' is the default is nothing else matches.
#       go)    $fstype and lots of internal parameters and regexs.
#       x)     Dies if a parameter required in @fsconfig is missing.
#
#
# There are some things I couldn't figure a way to parameterize by setting
# globals. $fstype is checked for filesystem type-specific logic, like 
# VMS directory syntax.
#
# Setting up for a particular OS type takes two steps: identify the OS and
# set all of the 'atomic' global variables, then take some of the atomic
# globals which are regexps and build composite values from them.
#
# The atomic regexp terms are generally used to build the larger composite
# regexps that recognize and break apart paths.  This leads to 
# two important rules for the atomic regexp terms:
#
# (1) Do not use '(' ... ')' in the regex terms, since they are used to build
# regexs that use '(' ... ')' to parse paths.
#
# (2) They must be built so that a '?' or other quantifier may be appended.
# This generally means using the '(?:' ... ')' or '[' ... ']' to group
# multicharacter patterns.  Other '(?' ... ')' may also do.
#
# The routines herein strive to preserve the
# original separator and root settings, and, it turns out, never need to
# prepend root to a string (although they do need to insert separators on
# occasion).  This is good, since the Win32 root expressions can be like
# '/', '\', 'A:/', 'a:/', or even '\\' or '//' for UNC style names.
#
# Note that the default root and default notsep are not used, and so are 
# undefined.
#
# For DOS, MacOS, and VMS, we assume that all paths handed in are on the same
# volume.  This is not a significant limitation except for abs2rel, since the
# absolute path is assumed to be on the same volume as the base path.
#
sub setfstype($;) {
   my( $osname ) = @_ ;

   # Find the best match for OS and set up our atomic globals accordingly
   if ( $osname =~ /^(?:(ms)?(dos|win(32|nt)?))/i )
   {
      $fstype           = 'Win32' ;
      $sep              = '/' ;
      $sepRE            = '[\\\\/]' ;
      $notsepRE         = '[^\\\\/]' ;
      $volumeRE         = '(?:^(?:[a-zA-Z]:|(?:\\\\\\\\|//)[^\\\\/]+[\\\\/][^\\\\/]+)?)' ;
      $directoryRE      = '(?:(?:.*[\\\\/](?:\.\.?$)?)?)' ;
      $isrootRE         = '(?:^[\\\\/])' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      $casesensitive    = 0 ;
      $idempotent       = 1 ;
   }
   elsif ( $osname =~ /^MacOS$/i )
   {
      $fstype           = 'MacOS' ;
      $sep              = ':' ;
      $sepRE            = '\:' ;
      $notsepRE         = '[^:]' ;
      $volumeRE         = '(?:^(?:.*::)?)' ;
      $directoryRE      = '(?:(?:.*:)?)' ;
      $isrootRE         = '(?:^:)' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      $casesensitive    = 0 ;
      $idempotent       = 1 ;
   }
   elsif ( $osname =~ /^VMS$/i )
   {
      $fstype           = 'VMS' ;
      $sep              = '.' ;
      $sepRE            = '[\.\]]' ;
      $notsepRE         = '[^\.\]]' ;
      # volume is node::volume:, where node:: and volume: are optional 
      # and node:: cannot be present without volume.  node can include
      # an access control string in double quotes.
      # Not supported:
      #     quoted full node names
      #     embedding a double quote in a string ("" to put " in)
      #     support ':' in node names
      #     foreign file specifications
      #     task specifications
      #     UIC Directory format (use the 6 digit name for it, instead)
      $volumeRE         = '(?:^(?:(?:[\w\$-]+(?:"[^"]*")?::)?[\w\$-]+:)?)' ;
      $directoryRE      = '(?:(?:\[.*\])?)' ;

      # Root is the lack of a leading '.', unless string is empty, which
      # means 'cwd', which is relative.
      $isrootRE         = '(?:^[^\.])' ;
      $thisDir          = '' ;
      $thisDirRE        = '\[\]' ;
      $parentDir        = '-' ;
      $parentDirRE      = '-' ;
      $casesensitive    = 0 ;
      $idempotent       = 0 ;
   }
   elsif ( $osname =~ /^URL$/i )
   {
      # URL spec based on RFC2396 (ftp://ftp.isi.edu/in-notes/rfc2396.txt)
      $fstype           = 'URL' ;
      $sep              = '/' ;
      $sepRE            = '/' ;
      $notsepRE         = '[^/]' ;
      # Volume= scheme + authority, both optional
      $volumeRE         = '(?:^(?:[a-zA-Z][a-zA-Z0-9+-.]*:)?(?://[^/?]*)?)' ;

      # Directories do _not_ include the query component: we pretend that 
      # anything after a "?" is the filename or part of it.  So a '/'
      # terminates and is part of the directory spec, while a '?' or '#'
      # terminate and are not part of the directory spec.
      #
      # We pretend that ";param" syntax does not exist
      #
      $directoryRE      = '(?:(?:[^?#]*/(?:\.\.?(?:$|(?=[?#])))?)?)' ;
      $isrootRE         = '(?:^/)' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      # Assume case sensitive, since many (most?) are.  The user can override 
      # this if they so desire.
      $casesensitive    = 1 ;
      $idempotent       = 1 ;
   }
   else
   { 
      $fstype           = 'Unix' ;
      $sep              = '/' ;
      $sepRE            = '/' ;
      $notsepRE         = '[^/]' ;
      $volumeRE         = '' ;
      $directoryRE      = '(?:(?:.*/(?:\.\.?$)?)?)' ;
      $isrootRE         = '(?:^/)' ;
      $thisDir          = '.' ;
      $thisDirRE        = '\.' ;
      $parentDir        = '..' ;
      $parentDirRE      = '(?:\.\.)' ;
      $casesensitive    = 1 ;
      $idempotent       = 1 ;
   }

   # Now set our composite regexps.

   # Maintain old name for backward compatibility
   $SL= $sep ;

   # Build lots of REs used below, so they don't need to be built every time
   # the routines that use them are called.
   $basenamesplitRE   = '^(.*)' . $sepRE . '(' . $notsepRE . '*)$' ;

   $leading_parentRE  = '(' . $isrootRE . '?)(?:' . $parentDirRE . $sepRE . ')*(?:' . $parentDirRE . '$)?' ;
   $trailing_sepRE    = '(.)' . $sepRE . $thisDirRE . '?$' ;

   $beginning_of_name = '(?:^|' . $isrootRE . '|' . $sepRE . ')' ;

   $dot_sep_etcRE     = 
      '(' . $beginning_of_name . ')(?:' . $thisDirRE . $sepRE . ')+';

   $name_sep_parentRE = 
      '(' . $beginning_of_name . ')'
      . '(?!(?:' . $thisDirRE . '|' . $parentDirRE . ')' . $sepRE . ')'
      . $notsepRE . '+' 
      . $sepRE . $parentDirRE 
      . '(?:' . $sepRE . '|$)'
      ;

   if ( $verbose ) {
      print( <<TOHERE )  ;
fstype        = "$fstype"
sep           = "$sep"
sepRE         = /$sepRE/
notsepRE      = /$notsepRE/
volumeRE      = /$volumeRE/
directoryRE   = /$directoryRE/
isrootRE      = /$isrootRE/
thisDir       = "$thisDir"
thisDirRE     = /$thisDirRE/
parentDir     = "$parentDir"
parentDirRE   = /$parentDirRE/
casesensitive = "$casesensitive"
TOHERE
   }

   return $fstype ;
}


setfstype( $^O ) ;


#
# splitpath: Splits a path into component parts: volume, dirpath, and filename.
#
#           Very much like File::Basename::fileparse(), but doesn't concern
#           itself with extensions and knows about volume names.
#
#           Returns ($volume, $directory, $filename ).
#
#           The contents of the returned list varies by operating system.
#
#           Unix:
#              $volume: always ''
#              $directory: up to, and including, final '/'
#              $filename: after final '/'
#
#           Win32:
#              $volume: drive letter and ':', if present
#              $directory and $filename are like on Unix, but '\' and '/' are
#              equivalent and the $volume is not in $directory..
#
#           VMS:
#              $volume: up to and including first ":"
#              $directory: "[...]" component
#              $filename: the rest.
#              $nofile is ignored
#
#           URL:
#              $volume: up to ':', then '//stuff/morestuff'.  No trailing '/'.
#              $directory: after $volume, up to last '/'
#              $filename: the rest.
#              $nofile is ignored
#
# Interface:
#       i)     $path
#       i)     $nofile: if true, then any trailing filename is assumed to
#              belong to the directory for non-VMS systems.
#       r)     list of ( $volume, $directory, $filename ).
#
sub splitpath {
   my( $path, $nofile )= @_ ;
   my( $volume, $directory, $file ) ;
   if ( $fstype ne 'VMS' && $fstype ne 'URL' && $nofile ) {
      $path =~ m/($volumeRE)(.*)$/ ;
      $volume   = $1 ;
      $directory= $2 ;
      $file     = '' ;
   }
   else {
      $path =~ m/($volumeRE)($directoryRE)(.*)$/ ;
      $volume   = $1 ;
      $directory= $2 ;
      $file     = $3 ;
   }

   # For Win32 UNC, force the directory portion to be non-empty. This is
   # because all UNC names are absolute, even if there's no trailing separator
   # after the sharename.
   #
   # This is a bit of a hack, necesitated by the implementation of $isrootRE,
   # which is only applied to the directory portion.
   #
   # A better long term solution might be to make the isroot test a member 
   # function in the future, object-oriented version of this.
   #
   $directory = $1
     if ( $fstype eq 'Win32' && $volume =~ /^($sepRE)$sepRE/ && $directory eq '' ) ;

   return ( $volume, $directory, $file ) ;
}


#
# joinpath: joins the results of splitpath().  Not really necessary now, but
# good to have:
#
#     - API completeness
#     - Self documenting code
#     - Future handling of other filesystems
#
# For instance, if you leave the ':' or the '[' and ']' out of VMS $volume
# and $directory strings, this patches it up.  If you leave out the '['
# and provide the ']', or vice versa, it is not cleaned up.  This is
# because it's useful to automatically insert both '[' and ']', but if you
# leave off only one, it's likely that there's a bug elsewhere that needs
# looking in to.
#
# Automatically inserts a separator between directory and filename if needed
# for non-VMS OSs.
#
# Automatically inserts a separator between volume and directory or file 
# if needed for Win32 UNC names.
#
sub joinpath($;$;$;) {
   my( $volume, $directory, $filename )= @_ ;

   # Fix up delimiters for $volume and $directory as needed for various OSs
   if ( $fstype eq 'VMS' ) {
      $volume .= ':'
         if ( $volume ne '' && $volume !~ m/:$/ ) ;

      $directory = join( '', ( '[', $directory, ']' ) )
         if ( $directory ne '' && $directory !~ m/^\[.*\]$/ ) ;
   }
   else {
      # Add trailing separator to directory names that require it and
      # need it.  URLs always require it if there are any directory
      # components.
      $directory .= $sep
         if (  $directory ne '' 
            && ( $fstype eq 'URL' || $filename ne '' )
            && $directory !~ m/$sepRE$/ 
            ) ;

      # Add trailing separator to volume for UNC and HTML volume
      # names that lack it and need it.
      # Note that if a URL volume is a scheme only (ends in ':'),
      # we don't require a separator: it's a relative URL.
      $volume .= $sep
         if (  (  ( $fstype eq 'Win32' && $volume =~ m#^$sepRE{2}# )
               || ( $fstype eq 'URL'   && $volume =~ m#[^:/]$#     )
               )
            && $volume    !~ m#$sepRE$#  
            && $directory !~ m#^$sepRE#      
            && ( $directory ne '' || $filename ne '' )
            ) ;
   }

   return join( '', $volume, $directory, $filename ) ;
}


#
# splitdirs: Splits a string containing directory portion of a path
# in to component parts.  Preserves trailing null entries, unlike split().
#
# "a/b" should get you [ 'a', 'b' ]
#
# "a/b/" should get you [ 'a', 'b', '' ]
#
# "/a/b/" should get you [ '', 'a', 'b', '' ]
#
# "a/b" returns the same array as 'a/////b' for those OSs where
# the seperator is idempotent (Unix and DOS, at least, but not VMS).
#
# Interface:
#     i) directory path string
#
sub splitdirs($;) {
   my( $directorypath )= @_ ;

   $directorypath =~ s/^\[(.*)\]$/$1/
      if ( $fstype eq 'VMS' ) ;

   #
   # split() likes to forget about trailing null fields, so here we
   # check to be sure that there will not be any before handling the
   # simple case.
   #
   return split( $sepRE, $directorypath )
      if ( $directorypath !~ m/$sepRE$/ ) ;

   #
   # since there was a trailing separator, add a file name to the end, then
   # do the split, then replace it with ''.
   #
   $directorypath.= "file" ;
   my( @directories )= split( $sepRE, $directorypath ) ;
   $directories[ $#directories ]= '' ;

   return @directories ;
}

#
# joindirs: Joins an array of directory names in to a string, adding
# OS-specific delimiters, like '[' and ']' for VMS.
#
# Note that empty strings '' are no different then non-empty strings,
# but that undefined strings are skipped by this algorithm.
#
# This is done the hard way to preserve separators that are already
# present in any of the directory names.
#
# Could this be made faster by using a join() followed 
# by s/($sepRE)$sepRE+/$1/g?
#
# Interface:
#     i) array of directory names
#     o) string representation of directory path
#
sub joindirs {
   my $directory_path ;

   $directory_path = shift
      while ( ! defined( $directory_path ) && @_ ) ;

   if ( ! defined( $directory_path ) ) {
      $directory_path = '' ;
   }
   else {
      local $_ ;

      for ( @_ ) {
        next if ( ! defined( $_ ) ) ;

        $directory_path .= $sep
           if ( $directory_path !~ /$sepRE$/ && ! /^$sepRE/ ) ;

        $directory_path .= $_ ;
      }
   }

   $directory_path = join( '', '[', $directory_path, ']' )
      if ( $fstype eq 'VMS' ) ;

   return $directory_path ;
}


#
# realpath: returns the canonicalized absolute path name
#
# Interface:
#       i)      $path   path
#       r)              resolved name on success else undef
#       go)     $resolved
#                       resolved name on success else the path name which
#                       caused the problem.
$resolved = '';
#
#       Note: this implementation is based 4.4BSD version realpath(3).
#
# TODO: Speed up by using Cwd::abs_path()?
#
sub realpath($;) {
    ($resolved) = @_;
    my($backdir) = cwd();
    my($dirname, $basename, $links, $reg);

    $resolved = regularize($resolved);
LOOP:
    {
        #
        # Find the dirname and basename.
        # Change directory to the dirname component.
        #
        if ($resolved =~ /$sepRE/) {
            ($dirname, $basename) = $resolved =~ /$basenamesplitRE/ ;
            $dirname = $sep if ( $dirname eq '' );
            $resolved = $dirname;
            unless (chdir($dirname)) {
                warn("realpath: chdir($dirname) failed: $! (in ${\cwd()}).") if $verbose;
                chdir($backdir);
                return undef;
            }
        } else {
            $dirname = '';
            $basename = $resolved;
        }
        #
        # If it is a symlink, read in the value and loop.
        # If it is a directory, then change to that directory.
        #
        if ( $basename ne '' ) {
            if (-l $basename) {
                unless ($resolved = readlink($basename)) {
                    warn("realpath: readlink($basename) failed: $! (in ${\cwd()}).") if $verbose;
                    chdir($backdir);
                    return undef;
                }
                $basename = '';
                if (++$links > $maxsymlinks) {
                    warn("realpath: too many symbolic links: $links.") if $verbose;
                    chdir($backdir);
                    return undef;
                }
                redo LOOP;
            } elsif (-d _) {
                unless (chdir($basename)) {
                    warn("realpath: chdir($basename) failed: $! (in ${\cwd()}).") if $verbose;
                    chdir($backdir);
                    return undef;
                }
                $basename = '';
            }
        }
    }
    #
    # Get the current directory name and append the basename.
    #
    $resolved = cwd();
    if ( $basename ne '' ) {
        $resolved .= $sep if ($resolved ne $sep);
        $resolved .= $basename
    }
    chdir($backdir);
    return $resolved;
} # end sub realpath


#
# abs2rel: make a relative pathname from an absolute pathname
#
# Interface:
#       i)      $path   absolute path(needed)
#       i)      $base   base directory(optional)
#       r)              relative path of $path
#
#       Note:   abs2rel doesn't check whether the specified path exist or not.
#
sub abs2rel($;$;) {
    my($path, $base) = @_;
    my($reg );

    my( $path_volume, $path_directory, $path_file )= splitpath( $path,'nofile');
    if ( $path_directory !~ /$isrootRE/ ) {
        warn("abs2rel: nothing to do: '$path' is relative.") if $verbose;
        return $path;
    }

    $base = cwd()
       if ( $base eq '' ) ;

    my( $base_volume, $base_directory, $base_file )= splitpath( $base,'nofile');
    # check for a filename, since the nofile parameter does not work for OSs
    # like VMS that have explicit delimiters between the dir and file portions
    warn( "abs2rel: filename '$base_file' passed in \$base" )
       if ( $base_file ne '' && $verbose ) ;

    if ( $base_directory !~ /$isrootRE/ ) {
        # Make $base absolute
        my( $cw_volume, $cw_directory, $dummy ) = splitpath( cwd(), 'nofile' ) ;
        # maybe we should warn if $cw_volume ne $base_volume and both are not ''
        $base_volume= $cw_volume
           if ( $base_volume eq '' && $cw_volume ne '' ) ;
        $base_directory = join( '', $cw_directory, $sep, $base_directory ) ;
    }

#print( "[$path_directory,$base_directory]\n" ) ;
    $path_directory = regularize( $path_directory );
    $base_directory = regularize( $base_directory );
#print( "[$path_directory,$base_directory]\n" ) ;
    # Now, remove all leading components that are the same, so 'name/a'
    # 'name/b' become 'a' and 'b'.
    my @pathchunks = split($sepRE, $path_directory);
    my @basechunks = split($sepRE, $base_directory);

    if ( $casesensitive ) 
    {
        while (@pathchunks && @basechunks && $pathchunks[0] eq $basechunks[0]) 
        {
            shift @pathchunks ;
            shift @basechunks ;
        }
    }
    else {
        while (  @pathchunks 
              && @basechunks 
              && lc( $pathchunks[0] ) eq lc( $basechunks[0] ) 
              ) 
        {
            shift @pathchunks ;
            shift @basechunks ;
        }
    }

    # No need to use joindirs() here, since we know that the arrays
    # are well formed.
    $path_directory= join( $sep, @pathchunks );
    $base_directory= join( $sep, @basechunks );
#print( "[$path_directory,$base_directory]\n" ) ;

    # Convert $base_directory from absolute to relative
    if ( $fstype eq 'VMS' ) {
        $base_directory= $sep . $base_directory
            if ( $base_directory ne '' ) ;
    }
    else {
        $base_directory=~ s/^$sepRE// ;
    }

#print( "[$base_directory]\n" ) ;
    # $base_directory now contains the directories the resulting relative path 
    # must ascend out of before it can descend to $path_directory.  So, 
    # replace all names with $parentDir
    $base_directory =~ s/$notsepRE+/$parentDir/g ;
#print( "[$base_directory]\n" ) ;

    # Glue the two together, using a separator if necessary, and preventing an
    # empty result.
    if ( $path_directory ne '' && $base_directory ne '' ) {
        $path_directory = "$base_directory$sep$path_directory" ;
    } else {
        $path_directory = "$base_directory$path_directory" ;
    }

    $path_directory = regularize( $path_directory ) ;

    # relative URLs should have no name in the volume, only a scheme.
    $path_volume=~ s#/.*##
        if ( $fstype eq 'URL' ) ;
    return joinpath( $path_volume, $path_directory, $path_file ) ;
}

#
# rel2abs: make an absolute pathname from a relative pathname
#
# Assumes no trailing file name on $base.  Ignores it if present on an OS
# like $VMS.
#
# Interface:
#       i)      $path   relative path (needed)
#       i)      $base   base directory  (optional)
#       r)              absolute path of $path
#
#       Note:   rel2abs doesn't check if the paths exist.
#
sub rel2abs($;$;) {
    my( $path, $base ) = @_;
    my( $reg );

    my( $path_volume, $path_directory, $path_file )= splitpath( $path, 'nofile' ) ;
    if ( $path_directory =~ /$isrootRE/ ) {
        warn( "rel2abs: nothing to do: '$path' is absolute" ) 
            if $verbose;
        return $path;
    }

    warn( "rel2abs: volume '$path_volume' passed in relative path: \$path" )
        if ( $path_volume ne '' && $verbose ) ;

    $base = cwd()
        if ( !defined( $base ) || $base eq '' ) ;

    my( $base_volume, $base_directory, $base_file )= splitpath( $base, 'nofile' ) ;
    # check for a filename, since the nofile parameter does not work for OSs
    # like VMS that have explicit delimiters between the dir and file portions
    warn( "rel2abs: filename '$base_file' passed in \$base" )
        if ( $base_file ne '' && $verbose ) ;

    if ( $base_directory !~ /$isrootRE/ ) {
        # Make $base absolute
        my( $cw_volume, $cw_directory, $dummy ) = splitpath( cwd(), 'nofile' ) ;
        # maybe we should warn if $cw_volume ne $base_volume and both are not ''
        $base_volume= $cw_volume
            if ( $base_volume eq '' && $cw_volume ne '' ) ;
        $base_directory = join( '', $cw_directory, $sep, $base_directory ) ;
    }

    $path_directory = regularize( $path_directory );
    $base_directory = regularize( $base_directory );

    my $result_directory ;
    # Avoid using a separator if either directory component is empty.
    if ( $base_directory ne '' && $path_directory ne '' ) {
        $result_directory= joindirs( $base_directory, $path_directory ) ;
    }
    else {
        $result_directory= "$base_directory$path_directory" ;
    }

    $result_directory = regularize( $result_directory );

    return joinpath( $base_volume, $result_directory, $path_file ) ;
}

#
# regularize a path.  
#
#    Removes dubious and redundant information. 
#    should only be called on directory portion on OSs
#    with volumes and with delimeters that separate dir names from file names,
#    since the separators can take on different semantics, like "\\" for UNC
#    under Win32, or '.' in filenames under VMS.
#
sub regularize {
    my( $in )= $_[ 0 ] ;

    # Combine idempotent separators.  Do this first so all other REs only
    # need to match one separator. Use the first sep found instead of
    # sepRE to preserve slashes on Win32.
    $in =~ s/($sepRE)$sepRE+/$1/g
        if ( $idempotent ) ;

    # We do this after deleting redundant separators in order to be consistent.
    # If a Win32 path ended in \/, we want to be sure that the \ is returned,
    # no the /.
    $in =~ /($sepRE)$sepRE*$/ ;
    my $trailing_sep = defined( $1 ) ? $1 : '' ;

    # Delete all occurences of 'name/..(/|$)'.  This is done with a while
    # loop to get rid of things like 'name1/name2/../..'. We chose the pattern
    # name/../ as the target instead of /name/.. so as to preserve 'rootness'.
    while ($in =~ s/$name_sep_parentRE/$1/g) {}
   
    # Get rid of ./ in '^./' and '/./'
    $in =~ s/$dot_sep_etcRE/$1/g ;

    # Get rid of trailing '/' and '/.' unless it would leave an empty string
    $in =~ s/$trailing_sepRE/$1/ ;

    # Get rid of '../' constructs from absolute paths
    $in =~ s/$leading_parentRE/$1/
      if ( $in =~ /$isrootRE/ ) ;

#    # Default to current directory if it's now empty.
#    $in = $thisDir if $_[0] eq '' ;
#
    # Restore trailing separator if it was lost. We do this to preserve
    # the 'dir-ness' of the path: paths that ended in a separator on entry
    # should leave with one in case the caller is using trailing slashes to
    # indicate paths to directories.
    $in .= $trailing_sep
        if ( $trailing_sep ne '' && $in !~ /$sepRE$/ ) ;

    return $in ;
}

1;

__END__

=head1 NAME

abs2rel - convert an absolute path to a relative path

rel2abs - convert a relative path to an absolute path

realpath - convert a logical path to a physical path (resolve symlinks)

splitpath - split a path in to volume, directory and filename components

joinpath - join volume, directory, and filename components to form a path

splitdirs - split directory specification in to component names

joindirs - join component names in to a directory specification

setfstype - set the file system type


=head1 SYNOPSIS

    use File::PathConvert qw(realpath abs2rel rel2abs setfstype splitpath 
      joinpath splitdirs joindirs $resolved);

    $relpath = abs2rel($abspath);
    $abspath = abs2rel($abspath, $base);

    $abspath = rel2abs($relpath);
    $abspath = rel2abs($relpath, $base);

    $path = realpath($logpath) || die "resolution stopped at $resolved";

    ( $volume, $directory, $filename )= splitpath( $path ) ;
    ( $volume, $directory, $filename )= splitpath( $path, 'nofile' ) ;

    $path= joinpath( $volume, $directory, $filename ) ;

    @directories= splitdirs( $directory ) ;
    $directory= joindirs( @directories ) ;

=head1 DESCRIPTION

File::PathConvert provides functions to convert between absolute and
relative paths, and from logical paths to physical paths on a variety of
filesystems, including the URL 'filesystem'.

Paths are decomposed internally in to volume, directory, and, sometimes
filename portions as appropriate to the operation and filesystem, then
recombined.  This preserves the volume and filename portions so that they may
be returned, and prevents them from interfering with the path conversions.  

Here are some examples of path decomposition.  A '****' in a column indicates
the column is not used in C<abs2rel> and C<rel2abs> functions for that
filesystem type.


    FS      VOLUME                  Directory       filename
    ======= ======================= =============== =============
    URL     http:                   /a/b/           c?query
            http://fubar.com        /a/b/           c?query
            //p.d.q.com             /a/b/c/         ?query

    VMS     Server::Volume:         [a.b]           c
            Server"access spec"::   [a.b]           c
            Volume:                 [a.b]           c

    Win32   A:                      \a\b\c          ****
            \\server\Volume         \a\b\c          ****
            \\server\Volume         \a/b/c          ****

    Unix    ****                    \a\b\c          ****

    MacOS   Volume::                a:b:c           ****

Many more examples abound in the test.pl included with this module.

Only the VMS and URL filesystems indicate if the last name in a path is a
directory or file.  For other filesystems, all non-volume names are assumed to
be directory names.  For URLs, the last name in a path is assumed to be a
filename unless it ends in '/', '/.', or '/..'.   

Other assumptions are made as well, especially MacOS and VMS. THESE MAY CHANGE
BASED ON PROGRAMMER FEEDBACK!

The conversion routines C<abs2rel>, C<rel2abs>, and C<realpath> are the 
main focus of this package.  C<splitpath> and C<joinpath> are provided to 
allow volume oriented filesystems (almost anything non-unixian, actually)
to be accomodated.  C<splitdirs> and C<joindirs> provide directory path
grammar parsing and encoding, which is especially useful for VMS.

=over 4

=item setfstype

This is called automatically on module load to set the filesystem type
according to $^O. The user can call this later set the filesystem type
manually.  If the name is not recognized, unix defaults are used.  Names
matching /^URL$/i, /^VMS$/i, /^MacOS$/i, or /^(ms)?(win|dos)/32|nt)?$/i yield
the appropriate (hopefully) filesystem settings.  These strings may be
generalized in the future.

Examples:

    File::PathConvert::setfstype( 'url' ) ; 
    File::PathConvert::setfstype( 'Win32' ) ;
    File::PathConvert::setfstype( 'HAL9000' ) ; # Results in Unix default

=item abs2rel

C<abs2rel> converts an absolute path name to a relative path:
converting /1/2/3/a/b/c relative to /1/2/3 returns a/b/c

    $relpath= abs2rel( $abspath ) ;
    $relpath= abs2rel( $abspath, $base ) ;

If $abspath is already relative, it is returned unchanged.  Otherwise the
relative path from $base to $abspath is returned.  If $base is undefined the
current directory is used.

The volume and filename portions of $base are ignored if present.  
If $abspath and $base are on different volumes, the volume from $abspath is
used.

No filesystem calls are made except for getting the current working directory
if $base is undefined, so symbolic links are not checked for or resolved, and
no check is done for existance.

Examples

    # Unix
    'a/b/c' == abs2rel( 'a/b/c', $anything )
    'a/b/c' == abs2rel( '/1/2/3/a/b/c', '/1/2/3' )

    # DOS
    'a\\b/c' == abs2rel( 'a\\b/c', $anything )
    'a\\b/c' == abs2rel( '/1\\2/3/a\\b/c', '/1/2/3' )

    # URL
    'http:a/b/c'           == abs2rel( 'http:a/b/c', $anything ) 
    'http:a/b/c'           == abs2rel( 'http:/1/2/3/a/b/c',
                                       'ftp://t.org/1/2/3/?z' )
    'http:a/b/c?q'         == abs2rel( 'http:/1/2/3/a/b/c/?q',
                                       'ftp://t.org/1/2/3?z'  )
    'http://s.com/a/b/c?q' == abs2rel( 'http://s.com/1/2/3/a/b/c?q',
                                       'ftp://t.org/1/2/3/?z')

=item rel2abs

C<rel2abs> makes converts a relative path name to an absolute path: 
converting a/b/c relative to /1/2/3 returns /1/2/3/a/b/c.

    $abspath= rel2abs( $relpath ) ;
    $abspath= rel2abs( $relpath, $base ) ;

If $relpath is already absolute, it is returned unchanged.  Otherwise $relpath
is taken to be relative to $base and the resulting absolute path is returned.
If $base is not supplied, the current working directory is used.

The volume portion of $relpath is ignored.  The filename portion of $base is
also ignored. The volume from $base is returned if present. The filename
portion of $abspath is returned if present.

No filesystem calls are made except for getting the current working directory
if $base is undefined, so symbolic links are not checked for or resolved, and
no check is done for existance.

C<rel2abs> will not return a path of the form "./file".

Examples

    # Unix
    '/a/b/c'       == rel2abs( '/a/b/c', $anything )
    '/1/2/3/a/b/c' == rel2abs( 'a/b/c', '/1/2/3' )

    # DOS
    '\\a\\b/c'                == rel2abs( '\\a\\b/c', $anything )
    '/1\\2/3\\a\\b/c'         == rel2abs( 'a\\b/c', '/1\\2/3' )
    'C:/1\\2/3\\a\\b/c'       == rel2abs( 'D:a\\b/c', 'C:/1\\2/3' )
    '\\\\s\\v/1\\2/3\\a\\b/c' == rel2abs( 'D:a\\b/c', '\\\\s\\v/1\\2/3' )

    # URL
    'http:/a/b/c?q'            == rel2abs( 'http:/a/b/c?q', $anything )
    'ftp://t.org/1/2/3/a/b/c?q'== rel2abs( 'http:a/b/c?q',
                                           'ftp://t.org/1/2/3?z' )


=item realpath

C<realpath> makes a canonicalized absolute pathname and
resolves all symbolic links, extra ``/'' characters, and references
to /./ and /../ in the path.
C<realpath> resolves both absolute and relative paths.
It returns the resolved name on success, otherwise it returns undef
and sets the valiable C<$File::PathConvert::resolved> to the pathname
that caused the problem.

All but the last component of the path must exist.

This implementation is based on 4.4BSD realpath(3).  It is not tested under
other operating systems at this time.

If '/sys' is a symbolic link to '/usr/src/sys':

    chdir('/usr');
    '/usr/src/sys/kern' == realpath('../sys/kern');
    '/usr/src/sys/kern' == realpath('/sys/kern');

=item splitpath

To be written...

=item joinpath

To be written...

Note that C<joinpath( splitpath( $path ) )> usually yields path.  URLs
with directory components ending in '/.' or '/..' will be fixed 
up to end in '/./' and '/../'.

=item splitdirs

To be written...

=item joindirs


=back

=head1 BUGS

C<realpath> is not fully multiplatform.


=head1 LIMITATIONS

=over 4

=item *

In URLs, paths not ending in '/' are split such that the last name in the
path is a filename.  This is not intuitive: many people use such URLs for
directories, and most servers send a redirect.  This may cause programmers
using this package to code in bugs, it may be more pragmatic to always assume
all names are directory names.  (Note that the query portion is always part
of the filename).

=item *

If the relative and base paths are on different volumes, no error is
returned.  A silent, hopefully reasonable assumption is made.

=item *

No detection of unix style paths is done when other filesystems are
selected, like File::Basename does.

=back

=head1 AUTHORS

Barrie Slaymaker <rbs@telerama.com>
Shigio Yamaguchi <shigio@wafu.netgate.net>

=cut
