1# Module 'ntpath' -- common operations on WinNT/Win95 pathnames
2"""Common pathname manipulations, WindowsNT/95 version.
3
4Instead of importing this module directly, import os and refer to this
5module as os.path.
6"""
7
8# strings representing various path-related bits and pieces
9# These are primarily for export; internally, they are hardcoded.
10# Should be set before imports for resolving cyclic dependency.
11curdir = '.'
12pardir = '..'
13extsep = '.'
14sep = '\\'
15pathsep = ';'
16altsep = '/'
17defpath = '.;C:\\bin'
18devnull = 'nul'
19
20import os
21import sys
22import stat
23import genericpath
24from genericpath import *
25
26
27__all__ = ["normcase","isabs","join","splitdrive","split","splitext",
28           "basename","dirname","commonprefix","getsize","getmtime",
29           "getatime","getctime", "islink","exists","lexists","isdir","isfile",
30           "ismount", "expanduser","expandvars","normpath","abspath",
31           "curdir","pardir","sep","pathsep","defpath","altsep",
32           "extsep","devnull","realpath","supports_unicode_filenames","relpath",
33           "samefile", "sameopenfile", "samestat", "commonpath"]
34
35def _get_bothseps(path):
36    if isinstance(path, bytes):
37        return b'\\/'
38    else:
39        return '\\/'
40
41# Normalize the case of a pathname and map slashes to backslashes.
42# Other normalizations (such as optimizing '../' away) are not done
43# (this is done by normpath).
44
45try:
46    from _winapi import (
47        LCMapStringEx as _LCMapStringEx,
48        LOCALE_NAME_INVARIANT as _LOCALE_NAME_INVARIANT,
49        LCMAP_LOWERCASE as _LCMAP_LOWERCASE)
50
51    def normcase(s):
52        """Normalize case of pathname.
53
54        Makes all characters lowercase and all slashes into backslashes.
55        """
56        s = os.fspath(s)
57        if not s:
58            return s
59        if isinstance(s, bytes):
60            encoding = sys.getfilesystemencoding()
61            s = s.decode(encoding, 'surrogateescape').replace('/', '\\')
62            s = _LCMapStringEx(_LOCALE_NAME_INVARIANT,
63                               _LCMAP_LOWERCASE, s)
64            return s.encode(encoding, 'surrogateescape')
65        else:
66            return _LCMapStringEx(_LOCALE_NAME_INVARIANT,
67                                  _LCMAP_LOWERCASE,
68                                  s.replace('/', '\\'))
69except ImportError:
70    def normcase(s):
71        """Normalize case of pathname.
72
73        Makes all characters lowercase and all slashes into backslashes.
74        """
75        s = os.fspath(s)
76        if isinstance(s, bytes):
77            return os.fsencode(os.fsdecode(s).replace('/', '\\').lower())
78        return s.replace('/', '\\').lower()
79
80
81# Return whether a path is absolute.
82# Trivial in Posix, harder on Windows.
83# For Windows it is absolute if it starts with a slash or backslash (current
84# volume), or if a pathname after the volume-letter-and-colon or UNC-resource
85# starts with a slash or backslash.
86
87def isabs(s):
88    """Test whether a path is absolute"""
89    s = os.fspath(s)
90    if isinstance(s, bytes):
91        sep = b'\\'
92        altsep = b'/'
93        colon_sep = b':\\'
94    else:
95        sep = '\\'
96        altsep = '/'
97        colon_sep = ':\\'
98    s = s[:3].replace(altsep, sep)
99    # Absolute: UNC, device, and paths with a drive and root.
100    # LEGACY BUG: isabs("/x") should be false since the path has no drive.
101    if s.startswith(sep) or s.startswith(colon_sep, 1):
102        return True
103    return False
104
105
106# Join two (or more) paths.
107def join(path, *paths):
108    path = os.fspath(path)
109    if isinstance(path, bytes):
110        sep = b'\\'
111        seps = b'\\/'
112        colon = b':'
113    else:
114        sep = '\\'
115        seps = '\\/'
116        colon = ':'
117    try:
118        if not paths:
119            path[:0] + sep  #23780: Ensure compatible data type even if p is null.
120        result_drive, result_path = splitdrive(path)
121        for p in map(os.fspath, paths):
122            p_drive, p_path = splitdrive(p)
123            if p_path and p_path[0] in seps:
124                # Second path is absolute
125                if p_drive or not result_drive:
126                    result_drive = p_drive
127                result_path = p_path
128                continue
129            elif p_drive and p_drive != result_drive:
130                if p_drive.lower() != result_drive.lower():
131                    # Different drives => ignore the first path entirely
132                    result_drive = p_drive
133                    result_path = p_path
134                    continue
135                # Same drive in different case
136                result_drive = p_drive
137            # Second path is relative to the first
138            if result_path and result_path[-1] not in seps:
139                result_path = result_path + sep
140            result_path = result_path + p_path
141        ## add separator between UNC and non-absolute path
142        if (result_path and result_path[0] not in seps and
143            result_drive and result_drive[-1:] != colon):
144            return result_drive + sep + result_path
145        return result_drive + result_path
146    except (TypeError, AttributeError, BytesWarning):
147        genericpath._check_arg_types('join', path, *paths)
148        raise
149
150
151# Split a path in a drive specification (a drive letter followed by a
152# colon) and the path specification.
153# It is always true that drivespec + pathspec == p
154def splitdrive(p):
155    """Split a pathname into drive/UNC sharepoint and relative path specifiers.
156    Returns a 2-tuple (drive_or_unc, path); either part may be empty.
157
158    If you assign
159        result = splitdrive(p)
160    It is always true that:
161        result[0] + result[1] == p
162
163    If the path contained a drive letter, drive_or_unc will contain everything
164    up to and including the colon.  e.g. splitdrive("c:/dir") returns ("c:", "/dir")
165
166    If the path contained a UNC path, the drive_or_unc will contain the host name
167    and share up to but not including the fourth directory separator character.
168    e.g. splitdrive("//host/computer/dir") returns ("//host/computer", "/dir")
169
170    Paths cannot contain both a drive letter and a UNC path.
171
172    """
173    p = os.fspath(p)
174    if len(p) >= 2:
175        if isinstance(p, bytes):
176            sep = b'\\'
177            altsep = b'/'
178            colon = b':'
179            unc_prefix = b'\\\\?\\UNC\\'
180        else:
181            sep = '\\'
182            altsep = '/'
183            colon = ':'
184            unc_prefix = '\\\\?\\UNC\\'
185        normp = p.replace(altsep, sep)
186        if normp[0:2] == sep * 2:
187            # UNC drives, e.g. \\server\share or \\?\UNC\server\share
188            # Device drives, e.g. \\.\device or \\?\device
189            start = 8 if normp[:8].upper() == unc_prefix else 2
190            index = normp.find(sep, start)
191            if index == -1:
192                return p, p[:0]
193            index2 = normp.find(sep, index + 1)
194            if index2 == -1:
195                return p, p[:0]
196            return p[:index2], p[index2:]
197        if normp[1:2] == colon:
198            # Drive-letter drives, e.g. X:
199            return p[:2], p[2:]
200    return p[:0], p
201
202
203# Split a path in head (everything up to the last '/') and tail (the
204# rest).  After the trailing '/' is stripped, the invariant
205# join(head, tail) == p holds.
206# The resulting head won't end in '/' unless it is the root.
207
208def split(p):
209    """Split a pathname.
210
211    Return tuple (head, tail) where tail is everything after the final slash.
212    Either part may be empty."""
213    p = os.fspath(p)
214    seps = _get_bothseps(p)
215    d, p = splitdrive(p)
216    # set i to index beyond p's last slash
217    i = len(p)
218    while i and p[i-1] not in seps:
219        i -= 1
220    head, tail = p[:i], p[i:]  # now tail has no slashes
221    # remove trailing slashes from head, unless it's all slashes
222    head = head.rstrip(seps) or head
223    return d + head, tail
224
225
226# Split a path in root and extension.
227# The extension is everything starting at the last dot in the last
228# pathname component; the root is everything before that.
229# It is always true that root + ext == p.
230
231def splitext(p):
232    p = os.fspath(p)
233    if isinstance(p, bytes):
234        return genericpath._splitext(p, b'\\', b'/', b'.')
235    else:
236        return genericpath._splitext(p, '\\', '/', '.')
237splitext.__doc__ = genericpath._splitext.__doc__
238
239
240# Return the tail (basename) part of a path.
241
242def basename(p):
243    """Returns the final component of a pathname"""
244    return split(p)[1]
245
246
247# Return the head (dirname) part of a path.
248
249def dirname(p):
250    """Returns the directory component of a pathname"""
251    return split(p)[0]
252
253# Is a path a symbolic link?
254# This will always return false on systems where os.lstat doesn't exist.
255
256def islink(path):
257    """Test whether a path is a symbolic link.
258    This will always return false for Windows prior to 6.0.
259    """
260    try:
261        st = os.lstat(path)
262    except (OSError, ValueError, AttributeError):
263        return False
264    return stat.S_ISLNK(st.st_mode)
265
266# Being true for dangling symbolic links is also useful.
267
268def lexists(path):
269    """Test whether a path exists.  Returns True for broken symbolic links"""
270    try:
271        st = os.lstat(path)
272    except (OSError, ValueError):
273        return False
274    return True
275
276# Is a path a mount point?
277# Any drive letter root (eg c:\)
278# Any share UNC (eg \\server\share)
279# Any volume mounted on a filesystem folder
280#
281# No one method detects all three situations. Historically we've lexically
282# detected drive letter roots and share UNCs. The canonical approach to
283# detecting mounted volumes (querying the reparse tag) fails for the most
284# common case: drive letter roots. The alternative which uses GetVolumePathName
285# fails if the drive letter is the result of a SUBST.
286try:
287    from nt import _getvolumepathname
288except ImportError:
289    _getvolumepathname = None
290def ismount(path):
291    """Test whether a path is a mount point (a drive root, the root of a
292    share, or a mounted volume)"""
293    path = os.fspath(path)
294    seps = _get_bothseps(path)
295    path = abspath(path)
296    root, rest = splitdrive(path)
297    if root and root[0] in seps:
298        return (not rest) or (rest in seps)
299    if rest and rest in seps:
300        return True
301
302    if _getvolumepathname:
303        x = path.rstrip(seps)
304        y =_getvolumepathname(path).rstrip(seps)
305        return x.casefold() == y.casefold()
306    else:
307        return False
308
309
310# Expand paths beginning with '~' or '~user'.
311# '~' means $HOME; '~user' means that user's home directory.
312# If the path doesn't begin with '~', or if the user or $HOME is unknown,
313# the path is returned unchanged (leaving error reporting to whatever
314# function is called with the expanded path as argument).
315# See also module 'glob' for expansion of *, ? and [...] in pathnames.
316# (A function should also be defined to do full *sh-style environment
317# variable expansion.)
318
319def expanduser(path):
320    """Expand ~ and ~user constructs.
321
322    If user or $HOME is unknown, do nothing."""
323    path = os.fspath(path)
324    if isinstance(path, bytes):
325        tilde = b'~'
326    else:
327        tilde = '~'
328    if not path.startswith(tilde):
329        return path
330    i, n = 1, len(path)
331    while i < n and path[i] not in _get_bothseps(path):
332        i += 1
333
334    if 'USERPROFILE' in os.environ:
335        userhome = os.environ['USERPROFILE']
336    elif not 'HOMEPATH' in os.environ:
337        return path
338    else:
339        try:
340            drive = os.environ['HOMEDRIVE']
341        except KeyError:
342            drive = ''
343        userhome = join(drive, os.environ['HOMEPATH'])
344
345    if i != 1: #~user
346        target_user = path[1:i]
347        if isinstance(target_user, bytes):
348            target_user = os.fsdecode(target_user)
349        current_user = os.environ.get('USERNAME')
350
351        if target_user != current_user:
352            # Try to guess user home directory.  By default all user
353            # profile directories are located in the same place and are
354            # named by corresponding usernames.  If userhome isn't a
355            # normal profile directory, this guess is likely wrong,
356            # so we bail out.
357            if current_user != basename(userhome):
358                return path
359            userhome = join(dirname(userhome), target_user)
360
361    if isinstance(path, bytes):
362        userhome = os.fsencode(userhome)
363
364    return userhome + path[i:]
365
366
367# Expand paths containing shell variable substitutions.
368# The following rules apply:
369#       - no expansion within single quotes
370#       - '$$' is translated into '$'
371#       - '%%' is translated into '%' if '%%' are not seen in %var1%%var2%
372#       - ${varname} is accepted.
373#       - $varname is accepted.
374#       - %varname% is accepted.
375#       - varnames can be made out of letters, digits and the characters '_-'
376#         (though is not verified in the ${varname} and %varname% cases)
377# XXX With COMMAND.COM you can use any characters in a variable name,
378# XXX except '^|<>='.
379
380def expandvars(path):
381    """Expand shell variables of the forms $var, ${var} and %var%.
382
383    Unknown variables are left unchanged."""
384    path = os.fspath(path)
385    if isinstance(path, bytes):
386        if b'$' not in path and b'%' not in path:
387            return path
388        import string
389        varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
390        quote = b'\''
391        percent = b'%'
392        brace = b'{'
393        rbrace = b'}'
394        dollar = b'$'
395        environ = getattr(os, 'environb', None)
396    else:
397        if '$' not in path and '%' not in path:
398            return path
399        import string
400        varchars = string.ascii_letters + string.digits + '_-'
401        quote = '\''
402        percent = '%'
403        brace = '{'
404        rbrace = '}'
405        dollar = '$'
406        environ = os.environ
407    res = path[:0]
408    index = 0
409    pathlen = len(path)
410    while index < pathlen:
411        c = path[index:index+1]
412        if c == quote:   # no expansion within single quotes
413            path = path[index + 1:]
414            pathlen = len(path)
415            try:
416                index = path.index(c)
417                res += c + path[:index + 1]
418            except ValueError:
419                res += c + path
420                index = pathlen - 1
421        elif c == percent:  # variable or '%'
422            if path[index + 1:index + 2] == percent:
423                res += c
424                index += 1
425            else:
426                path = path[index+1:]
427                pathlen = len(path)
428                try:
429                    index = path.index(percent)
430                except ValueError:
431                    res += percent + path
432                    index = pathlen - 1
433                else:
434                    var = path[:index]
435                    try:
436                        if environ is None:
437                            value = os.fsencode(os.environ[os.fsdecode(var)])
438                        else:
439                            value = environ[var]
440                    except KeyError:
441                        value = percent + var + percent
442                    res += value
443        elif c == dollar:  # variable or '$$'
444            if path[index + 1:index + 2] == dollar:
445                res += c
446                index += 1
447            elif path[index + 1:index + 2] == brace:
448                path = path[index+2:]
449                pathlen = len(path)
450                try:
451                    index = path.index(rbrace)
452                except ValueError:
453                    res += dollar + brace + path
454                    index = pathlen - 1
455                else:
456                    var = path[:index]
457                    try:
458                        if environ is None:
459                            value = os.fsencode(os.environ[os.fsdecode(var)])
460                        else:
461                            value = environ[var]
462                    except KeyError:
463                        value = dollar + brace + var + rbrace
464                    res += value
465            else:
466                var = path[:0]
467                index += 1
468                c = path[index:index + 1]
469                while c and c in varchars:
470                    var += c
471                    index += 1
472                    c = path[index:index + 1]
473                try:
474                    if environ is None:
475                        value = os.fsencode(os.environ[os.fsdecode(var)])
476                    else:
477                        value = environ[var]
478                except KeyError:
479                    value = dollar + var
480                res += value
481                if c:
482                    index -= 1
483        else:
484            res += c
485        index += 1
486    return res
487
488
489# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
490# Previously, this function also truncated pathnames to 8+3 format,
491# but as this module is called "ntpath", that's obviously wrong!
492try:
493    from nt import _path_normpath
494
495except ImportError:
496    def normpath(path):
497        """Normalize path, eliminating double slashes, etc."""
498        path = os.fspath(path)
499        if isinstance(path, bytes):
500            sep = b'\\'
501            altsep = b'/'
502            curdir = b'.'
503            pardir = b'..'
504        else:
505            sep = '\\'
506            altsep = '/'
507            curdir = '.'
508            pardir = '..'
509        path = path.replace(altsep, sep)
510        prefix, path = splitdrive(path)
511
512        # collapse initial backslashes
513        if path.startswith(sep):
514            prefix += sep
515            path = path.lstrip(sep)
516
517        comps = path.split(sep)
518        i = 0
519        while i < len(comps):
520            if not comps[i] or comps[i] == curdir:
521                del comps[i]
522            elif comps[i] == pardir:
523                if i > 0 and comps[i-1] != pardir:
524                    del comps[i-1:i+1]
525                    i -= 1
526                elif i == 0 and prefix.endswith(sep):
527                    del comps[i]
528                else:
529                    i += 1
530            else:
531                i += 1
532        # If the path is now empty, substitute '.'
533        if not prefix and not comps:
534            comps.append(curdir)
535        return prefix + sep.join(comps)
536
537else:
538    def normpath(path):
539        """Normalize path, eliminating double slashes, etc."""
540        path = os.fspath(path)
541        if isinstance(path, bytes):
542            return os.fsencode(_path_normpath(os.fsdecode(path))) or b"."
543        return _path_normpath(path) or "."
544
545
546def _abspath_fallback(path):
547    """Return the absolute version of a path as a fallback function in case
548    `nt._getfullpathname` is not available or raises OSError. See bpo-31047 for
549    more.
550
551    """
552
553    path = os.fspath(path)
554    if not isabs(path):
555        if isinstance(path, bytes):
556            cwd = os.getcwdb()
557        else:
558            cwd = os.getcwd()
559        path = join(cwd, path)
560    return normpath(path)
561
562# Return an absolute path.
563try:
564    from nt import _getfullpathname
565
566except ImportError: # not running on Windows - mock up something sensible
567    abspath = _abspath_fallback
568
569else:  # use native Windows method on Windows
570    def abspath(path):
571        """Return the absolute version of a path."""
572        try:
573            return _getfullpathname(normpath(path))
574        except (OSError, ValueError):
575            return _abspath_fallback(path)
576
577try:
578    from nt import _getfinalpathname, readlink as _nt_readlink
579except ImportError:
580    # realpath is a no-op on systems without _getfinalpathname support.
581    realpath = abspath
582else:
583    def _readlink_deep(path):
584        # These error codes indicate that we should stop reading links and
585        # return the path we currently have.
586        # 1: ERROR_INVALID_FUNCTION
587        # 2: ERROR_FILE_NOT_FOUND
588        # 3: ERROR_DIRECTORY_NOT_FOUND
589        # 5: ERROR_ACCESS_DENIED
590        # 21: ERROR_NOT_READY (implies drive with no media)
591        # 32: ERROR_SHARING_VIOLATION (probably an NTFS paging file)
592        # 50: ERROR_NOT_SUPPORTED (implies no support for reparse points)
593        # 67: ERROR_BAD_NET_NAME (implies remote server unavailable)
594        # 87: ERROR_INVALID_PARAMETER
595        # 4390: ERROR_NOT_A_REPARSE_POINT
596        # 4392: ERROR_INVALID_REPARSE_DATA
597        # 4393: ERROR_REPARSE_TAG_INVALID
598        allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 67, 87, 4390, 4392, 4393
599
600        seen = set()
601        while normcase(path) not in seen:
602            seen.add(normcase(path))
603            try:
604                old_path = path
605                path = _nt_readlink(path)
606                # Links may be relative, so resolve them against their
607                # own location
608                if not isabs(path):
609                    # If it's something other than a symlink, we don't know
610                    # what it's actually going to be resolved against, so
611                    # just return the old path.
612                    if not islink(old_path):
613                        path = old_path
614                        break
615                    path = normpath(join(dirname(old_path), path))
616            except OSError as ex:
617                if ex.winerror in allowed_winerror:
618                    break
619                raise
620            except ValueError:
621                # Stop on reparse points that are not symlinks
622                break
623        return path
624
625    def _getfinalpathname_nonstrict(path):
626        # These error codes indicate that we should stop resolving the path
627        # and return the value we currently have.
628        # 1: ERROR_INVALID_FUNCTION
629        # 2: ERROR_FILE_NOT_FOUND
630        # 3: ERROR_DIRECTORY_NOT_FOUND
631        # 5: ERROR_ACCESS_DENIED
632        # 21: ERROR_NOT_READY (implies drive with no media)
633        # 32: ERROR_SHARING_VIOLATION (probably an NTFS paging file)
634        # 50: ERROR_NOT_SUPPORTED
635        # 53: ERROR_BAD_NETPATH
636        # 65: ERROR_NETWORK_ACCESS_DENIED
637        # 67: ERROR_BAD_NET_NAME (implies remote server unavailable)
638        # 87: ERROR_INVALID_PARAMETER
639        # 123: ERROR_INVALID_NAME
640        # 161: ERROR_BAD_PATHNAME
641        # 1920: ERROR_CANT_ACCESS_FILE
642        # 1921: ERROR_CANT_RESOLVE_FILENAME (implies unfollowable symlink)
643        allowed_winerror = 1, 2, 3, 5, 21, 32, 50, 53, 65, 67, 87, 123, 161, 1920, 1921
644
645        # Non-strict algorithm is to find as much of the target directory
646        # as we can and join the rest.
647        tail = path[:0]
648        while path:
649            try:
650                path = _getfinalpathname(path)
651                return join(path, tail) if tail else path
652            except OSError as ex:
653                if ex.winerror not in allowed_winerror:
654                    raise
655                try:
656                    # The OS could not resolve this path fully, so we attempt
657                    # to follow the link ourselves. If we succeed, join the tail
658                    # and return.
659                    new_path = _readlink_deep(path)
660                    if new_path != path:
661                        return join(new_path, tail) if tail else new_path
662                except OSError:
663                    # If we fail to readlink(), let's keep traversing
664                    pass
665                path, name = split(path)
666                # TODO (bpo-38186): Request the real file name from the directory
667                # entry using FindFirstFileW. For now, we will return the path
668                # as best we have it
669                if path and not name:
670                    return path + tail
671                tail = join(name, tail) if tail else name
672        return tail
673
674    def realpath(path, *, strict=False):
675        path = normpath(path)
676        if isinstance(path, bytes):
677            prefix = b'\\\\?\\'
678            unc_prefix = b'\\\\?\\UNC\\'
679            new_unc_prefix = b'\\\\'
680            cwd = os.getcwdb()
681            # bpo-38081: Special case for realpath(b'nul')
682            if normcase(path) == normcase(os.fsencode(devnull)):
683                return b'\\\\.\\NUL'
684        else:
685            prefix = '\\\\?\\'
686            unc_prefix = '\\\\?\\UNC\\'
687            new_unc_prefix = '\\\\'
688            cwd = os.getcwd()
689            # bpo-38081: Special case for realpath('nul')
690            if normcase(path) == normcase(devnull):
691                return '\\\\.\\NUL'
692        had_prefix = path.startswith(prefix)
693        if not had_prefix and not isabs(path):
694            path = join(cwd, path)
695        try:
696            path = _getfinalpathname(path)
697            initial_winerror = 0
698        except OSError as ex:
699            if strict:
700                raise
701            initial_winerror = ex.winerror
702            path = _getfinalpathname_nonstrict(path)
703        # The path returned by _getfinalpathname will always start with \\?\ -
704        # strip off that prefix unless it was already provided on the original
705        # path.
706        if not had_prefix and path.startswith(prefix):
707            # For UNC paths, the prefix will actually be \\?\UNC\
708            # Handle that case as well.
709            if path.startswith(unc_prefix):
710                spath = new_unc_prefix + path[len(unc_prefix):]
711            else:
712                spath = path[len(prefix):]
713            # Ensure that the non-prefixed path resolves to the same path
714            try:
715                if _getfinalpathname(spath) == path:
716                    path = spath
717            except OSError as ex:
718                # If the path does not exist and originally did not exist, then
719                # strip the prefix anyway.
720                if ex.winerror == initial_winerror:
721                    path = spath
722        return path
723
724
725# Win9x family and earlier have no Unicode filename support.
726supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and
727                              sys.getwindowsversion()[3] >= 2)
728
729def relpath(path, start=None):
730    """Return a relative version of a path"""
731    path = os.fspath(path)
732    if isinstance(path, bytes):
733        sep = b'\\'
734        curdir = b'.'
735        pardir = b'..'
736    else:
737        sep = '\\'
738        curdir = '.'
739        pardir = '..'
740
741    if start is None:
742        start = curdir
743
744    if not path:
745        raise ValueError("no path specified")
746
747    start = os.fspath(start)
748    try:
749        start_abs = abspath(normpath(start))
750        path_abs = abspath(normpath(path))
751        start_drive, start_rest = splitdrive(start_abs)
752        path_drive, path_rest = splitdrive(path_abs)
753        if normcase(start_drive) != normcase(path_drive):
754            raise ValueError("path is on mount %r, start on mount %r" % (
755                path_drive, start_drive))
756
757        start_list = [x for x in start_rest.split(sep) if x]
758        path_list = [x for x in path_rest.split(sep) if x]
759        # Work out how much of the filepath is shared by start and path.
760        i = 0
761        for e1, e2 in zip(start_list, path_list):
762            if normcase(e1) != normcase(e2):
763                break
764            i += 1
765
766        rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
767        if not rel_list:
768            return curdir
769        return join(*rel_list)
770    except (TypeError, ValueError, AttributeError, BytesWarning, DeprecationWarning):
771        genericpath._check_arg_types('relpath', path, start)
772        raise
773
774
775# Return the longest common sub-path of the sequence of paths given as input.
776# The function is case-insensitive and 'separator-insensitive', i.e. if the
777# only difference between two paths is the use of '\' versus '/' as separator,
778# they are deemed to be equal.
779#
780# However, the returned path will have the standard '\' separator (even if the
781# given paths had the alternative '/' separator) and will have the case of the
782# first path given in the sequence. Additionally, any trailing separator is
783# stripped from the returned path.
784
785def commonpath(paths):
786    """Given a sequence of path names, returns the longest common sub-path."""
787
788    if not paths:
789        raise ValueError('commonpath() arg is an empty sequence')
790
791    paths = tuple(map(os.fspath, paths))
792    if isinstance(paths[0], bytes):
793        sep = b'\\'
794        altsep = b'/'
795        curdir = b'.'
796    else:
797        sep = '\\'
798        altsep = '/'
799        curdir = '.'
800
801    try:
802        drivesplits = [splitdrive(p.replace(altsep, sep).lower()) for p in paths]
803        split_paths = [p.split(sep) for d, p in drivesplits]
804
805        try:
806            isabs, = set(p[:1] == sep for d, p in drivesplits)
807        except ValueError:
808            raise ValueError("Can't mix absolute and relative paths") from None
809
810        # Check that all drive letters or UNC paths match. The check is made only
811        # now otherwise type errors for mixing strings and bytes would not be
812        # caught.
813        if len(set(d for d, p in drivesplits)) != 1:
814            raise ValueError("Paths don't have the same drive")
815
816        drive, path = splitdrive(paths[0].replace(altsep, sep))
817        common = path.split(sep)
818        common = [c for c in common if c and c != curdir]
819
820        split_paths = [[c for c in s if c and c != curdir] for s in split_paths]
821        s1 = min(split_paths)
822        s2 = max(split_paths)
823        for i, c in enumerate(s1):
824            if c != s2[i]:
825                common = common[:i]
826                break
827        else:
828            common = common[:len(s1)]
829
830        prefix = drive + sep if isabs else drive
831        return prefix + sep.join(common)
832    except (TypeError, AttributeError):
833        genericpath._check_arg_types('commonpath', *paths)
834        raise
835
836
837try:
838    # The genericpath.isdir implementation uses os.stat and checks the mode
839    # attribute to tell whether or not the path is a directory.
840    # This is overkill on Windows - just pass the path to GetFileAttributes
841    # and check the attribute from there.
842    from nt import _isdir as isdir
843except ImportError:
844    # Use genericpath.isdir as imported above.
845    pass
846