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