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__all__ = ["normcase","isabs","join","splitdrive","split","splitext",
27           "basename","dirname","commonprefix","getsize","getmtime",
28           "getatime","getctime", "islink","exists","lexists","isdir","isfile",
29           "ismount", "expanduser","expandvars","normpath","abspath",
30           "curdir","pardir","sep","pathsep","defpath","altsep",
31           "extsep","devnull","realpath","supports_unicode_filenames","relpath",
32           "samefile", "sameopenfile", "samestat", "commonpath"]
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
44def normcase(s):
45    """Normalize case of pathname.
46
47    Makes all characters lowercase and all slashes into backslashes."""
48    s = os.fspath(s)
49    try:
50        if isinstance(s, bytes):
51            return s.replace(b'/', b'\\').lower()
52        else:
53            return s.replace('/', '\\').lower()
54    except (TypeError, AttributeError):
55        if not isinstance(s, (bytes, str)):
56            raise TypeError("normcase() argument must be str or bytes, "
57                            "not %r" % s.__class__.__name__) from None
58        raise
59
60
61# Return whether a path is absolute.
62# Trivial in Posix, harder on Windows.
63# For Windows it is absolute if it starts with a slash or backslash (current
64# volume), or if a pathname after the volume-letter-and-colon or UNC-resource
65# starts with a slash or backslash.
66
67def isabs(s):
68    """Test whether a path is absolute"""
69    s = os.fspath(s)
70    s = splitdrive(s)[1]
71    return len(s) > 0 and s[0] in _get_bothseps(s)
72
73
74# Join two (or more) paths.
75def join(path, *paths):
76    path = os.fspath(path)
77    if isinstance(path, bytes):
78        sep = b'\\'
79        seps = b'\\/'
80        colon = b':'
81    else:
82        sep = '\\'
83        seps = '\\/'
84        colon = ':'
85    try:
86        if not paths:
87            path[:0] + sep  #23780: Ensure compatible data type even if p is null.
88        result_drive, result_path = splitdrive(path)
89        for p in map(os.fspath, paths):
90            p_drive, p_path = splitdrive(p)
91            if p_path and p_path[0] in seps:
92                # Second path is absolute
93                if p_drive or not result_drive:
94                    result_drive = p_drive
95                result_path = p_path
96                continue
97            elif p_drive and p_drive != result_drive:
98                if p_drive.lower() != result_drive.lower():
99                    # Different drives => ignore the first path entirely
100                    result_drive = p_drive
101                    result_path = p_path
102                    continue
103                # Same drive in different case
104                result_drive = p_drive
105            # Second path is relative to the first
106            if result_path and result_path[-1] not in seps:
107                result_path = result_path + sep
108            result_path = result_path + p_path
109        ## add separator between UNC and non-absolute path
110        if (result_path and result_path[0] not in seps and
111            result_drive and result_drive[-1:] != colon):
112            return result_drive + sep + result_path
113        return result_drive + result_path
114    except (TypeError, AttributeError, BytesWarning):
115        genericpath._check_arg_types('join', path, *paths)
116        raise
117
118
119# Split a path in a drive specification (a drive letter followed by a
120# colon) and the path specification.
121# It is always true that drivespec + pathspec == p
122def splitdrive(p):
123    """Split a pathname into drive/UNC sharepoint and relative path specifiers.
124    Returns a 2-tuple (drive_or_unc, path); either part may be empty.
125
126    If you assign
127        result = splitdrive(p)
128    It is always true that:
129        result[0] + result[1] == p
130
131    If the path contained a drive letter, drive_or_unc will contain everything
132    up to and including the colon.  e.g. splitdrive("c:/dir") returns ("c:", "/dir")
133
134    If the path contained a UNC path, the drive_or_unc will contain the host name
135    and share up to but not including the fourth directory separator character.
136    e.g. splitdrive("//host/computer/dir") returns ("//host/computer", "/dir")
137
138    Paths cannot contain both a drive letter and a UNC path.
139
140    """
141    p = os.fspath(p)
142    if len(p) >= 2:
143        if isinstance(p, bytes):
144            sep = b'\\'
145            altsep = b'/'
146            colon = b':'
147        else:
148            sep = '\\'
149            altsep = '/'
150            colon = ':'
151        normp = p.replace(altsep, sep)
152        if (normp[0:2] == sep*2) and (normp[2:3] != sep):
153            # is a UNC path:
154            # vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
155            # \\machine\mountpoint\directory\etc\...
156            #           directory ^^^^^^^^^^^^^^^
157            index = normp.find(sep, 2)
158            if index == -1:
159                return p[:0], p
160            index2 = normp.find(sep, index + 1)
161            # a UNC path can't have two slashes in a row
162            # (after the initial two)
163            if index2 == index + 1:
164                return p[:0], p
165            if index2 == -1:
166                index2 = len(p)
167            return p[:index2], p[index2:]
168        if normp[1:2] == colon:
169            return p[:2], p[2:]
170    return p[:0], p
171
172
173# Split a path in head (everything up to the last '/') and tail (the
174# rest).  After the trailing '/' is stripped, the invariant
175# join(head, tail) == p holds.
176# The resulting head won't end in '/' unless it is the root.
177
178def split(p):
179    """Split a pathname.
180
181    Return tuple (head, tail) where tail is everything after the final slash.
182    Either part may be empty."""
183    p = os.fspath(p)
184    seps = _get_bothseps(p)
185    d, p = splitdrive(p)
186    # set i to index beyond p's last slash
187    i = len(p)
188    while i and p[i-1] not in seps:
189        i -= 1
190    head, tail = p[:i], p[i:]  # now tail has no slashes
191    # remove trailing slashes from head, unless it's all slashes
192    head = head.rstrip(seps) or head
193    return d + head, tail
194
195
196# Split a path in root and extension.
197# The extension is everything starting at the last dot in the last
198# pathname component; the root is everything before that.
199# It is always true that root + ext == p.
200
201def splitext(p):
202    p = os.fspath(p)
203    if isinstance(p, bytes):
204        return genericpath._splitext(p, b'\\', b'/', b'.')
205    else:
206        return genericpath._splitext(p, '\\', '/', '.')
207splitext.__doc__ = genericpath._splitext.__doc__
208
209
210# Return the tail (basename) part of a path.
211
212def basename(p):
213    """Returns the final component of a pathname"""
214    return split(p)[1]
215
216
217# Return the head (dirname) part of a path.
218
219def dirname(p):
220    """Returns the directory component of a pathname"""
221    return split(p)[0]
222
223# Is a path a symbolic link?
224# This will always return false on systems where os.lstat doesn't exist.
225
226def islink(path):
227    """Test whether a path is a symbolic link.
228    This will always return false for Windows prior to 6.0.
229    """
230    try:
231        st = os.lstat(path)
232    except (OSError, AttributeError):
233        return False
234    return stat.S_ISLNK(st.st_mode)
235
236# Being true for dangling symbolic links is also useful.
237
238def lexists(path):
239    """Test whether a path exists.  Returns True for broken symbolic links"""
240    try:
241        st = os.lstat(path)
242    except OSError:
243        return False
244    return True
245
246# Is a path a mount point?
247# Any drive letter root (eg c:\)
248# Any share UNC (eg \\server\share)
249# Any volume mounted on a filesystem folder
250#
251# No one method detects all three situations. Historically we've lexically
252# detected drive letter roots and share UNCs. The canonical approach to
253# detecting mounted volumes (querying the reparse tag) fails for the most
254# common case: drive letter roots. The alternative which uses GetVolumePathName
255# fails if the drive letter is the result of a SUBST.
256try:
257    from nt import _getvolumepathname
258except ImportError:
259    _getvolumepathname = None
260def ismount(path):
261    """Test whether a path is a mount point (a drive root, the root of a
262    share, or a mounted volume)"""
263    path = os.fspath(path)
264    seps = _get_bothseps(path)
265    path = abspath(path)
266    root, rest = splitdrive(path)
267    if root and root[0] in seps:
268        return (not rest) or (rest in seps)
269    if rest in seps:
270        return True
271
272    if _getvolumepathname:
273        return path.rstrip(seps) == _getvolumepathname(path).rstrip(seps)
274    else:
275        return False
276
277
278# Expand paths beginning with '~' or '~user'.
279# '~' means $HOME; '~user' means that user's home directory.
280# If the path doesn't begin with '~', or if the user or $HOME is unknown,
281# the path is returned unchanged (leaving error reporting to whatever
282# function is called with the expanded path as argument).
283# See also module 'glob' for expansion of *, ? and [...] in pathnames.
284# (A function should also be defined to do full *sh-style environment
285# variable expansion.)
286
287def expanduser(path):
288    """Expand ~ and ~user constructs.
289
290    If user or $HOME is unknown, do nothing."""
291    path = os.fspath(path)
292    if isinstance(path, bytes):
293        tilde = b'~'
294    else:
295        tilde = '~'
296    if not path.startswith(tilde):
297        return path
298    i, n = 1, len(path)
299    while i < n and path[i] not in _get_bothseps(path):
300        i += 1
301
302    if 'HOME' in os.environ:
303        userhome = os.environ['HOME']
304    elif 'USERPROFILE' in os.environ:
305        userhome = os.environ['USERPROFILE']
306    elif not 'HOMEPATH' in os.environ:
307        return path
308    else:
309        try:
310            drive = os.environ['HOMEDRIVE']
311        except KeyError:
312            drive = ''
313        userhome = join(drive, os.environ['HOMEPATH'])
314
315    if isinstance(path, bytes):
316        userhome = os.fsencode(userhome)
317
318    if i != 1: #~user
319        userhome = join(dirname(userhome), path[1:i])
320
321    return userhome + path[i:]
322
323
324# Expand paths containing shell variable substitutions.
325# The following rules apply:
326#       - no expansion within single quotes
327#       - '$$' is translated into '$'
328#       - '%%' is translated into '%' if '%%' are not seen in %var1%%var2%
329#       - ${varname} is accepted.
330#       - $varname is accepted.
331#       - %varname% is accepted.
332#       - varnames can be made out of letters, digits and the characters '_-'
333#         (though is not verified in the ${varname} and %varname% cases)
334# XXX With COMMAND.COM you can use any characters in a variable name,
335# XXX except '^|<>='.
336
337def expandvars(path):
338    """Expand shell variables of the forms $var, ${var} and %var%.
339
340    Unknown variables are left unchanged."""
341    path = os.fspath(path)
342    if isinstance(path, bytes):
343        if b'$' not in path and b'%' not in path:
344            return path
345        import string
346        varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
347        quote = b'\''
348        percent = b'%'
349        brace = b'{'
350        rbrace = b'}'
351        dollar = b'$'
352        environ = getattr(os, 'environb', None)
353    else:
354        if '$' not in path and '%' not in path:
355            return path
356        import string
357        varchars = string.ascii_letters + string.digits + '_-'
358        quote = '\''
359        percent = '%'
360        brace = '{'
361        rbrace = '}'
362        dollar = '$'
363        environ = os.environ
364    res = path[:0]
365    index = 0
366    pathlen = len(path)
367    while index < pathlen:
368        c = path[index:index+1]
369        if c == quote:   # no expansion within single quotes
370            path = path[index + 1:]
371            pathlen = len(path)
372            try:
373                index = path.index(c)
374                res += c + path[:index + 1]
375            except ValueError:
376                res += c + path
377                index = pathlen - 1
378        elif c == percent:  # variable or '%'
379            if path[index + 1:index + 2] == percent:
380                res += c
381                index += 1
382            else:
383                path = path[index+1:]
384                pathlen = len(path)
385                try:
386                    index = path.index(percent)
387                except ValueError:
388                    res += percent + path
389                    index = pathlen - 1
390                else:
391                    var = path[:index]
392                    try:
393                        if environ is None:
394                            value = os.fsencode(os.environ[os.fsdecode(var)])
395                        else:
396                            value = environ[var]
397                    except KeyError:
398                        value = percent + var + percent
399                    res += value
400        elif c == dollar:  # variable or '$$'
401            if path[index + 1:index + 2] == dollar:
402                res += c
403                index += 1
404            elif path[index + 1:index + 2] == brace:
405                path = path[index+2:]
406                pathlen = len(path)
407                try:
408                    index = path.index(rbrace)
409                except ValueError:
410                    res += dollar + brace + path
411                    index = pathlen - 1
412                else:
413                    var = path[:index]
414                    try:
415                        if environ is None:
416                            value = os.fsencode(os.environ[os.fsdecode(var)])
417                        else:
418                            value = environ[var]
419                    except KeyError:
420                        value = dollar + brace + var + rbrace
421                    res += value
422            else:
423                var = path[:0]
424                index += 1
425                c = path[index:index + 1]
426                while c and c in varchars:
427                    var += c
428                    index += 1
429                    c = path[index:index + 1]
430                try:
431                    if environ is None:
432                        value = os.fsencode(os.environ[os.fsdecode(var)])
433                    else:
434                        value = environ[var]
435                except KeyError:
436                    value = dollar + var
437                res += value
438                if c:
439                    index -= 1
440        else:
441            res += c
442        index += 1
443    return res
444
445
446# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
447# Previously, this function also truncated pathnames to 8+3 format,
448# but as this module is called "ntpath", that's obviously wrong!
449
450def normpath(path):
451    """Normalize path, eliminating double slashes, etc."""
452    path = os.fspath(path)
453    if isinstance(path, bytes):
454        sep = b'\\'
455        altsep = b'/'
456        curdir = b'.'
457        pardir = b'..'
458        special_prefixes = (b'\\\\.\\', b'\\\\?\\')
459    else:
460        sep = '\\'
461        altsep = '/'
462        curdir = '.'
463        pardir = '..'
464        special_prefixes = ('\\\\.\\', '\\\\?\\')
465    if path.startswith(special_prefixes):
466        # in the case of paths with these prefixes:
467        # \\.\ -> device names
468        # \\?\ -> literal paths
469        # do not do any normalization, but return the path unchanged
470        return path
471    path = path.replace(altsep, sep)
472    prefix, path = splitdrive(path)
473
474    # collapse initial backslashes
475    if path.startswith(sep):
476        prefix += sep
477        path = path.lstrip(sep)
478
479    comps = path.split(sep)
480    i = 0
481    while i < len(comps):
482        if not comps[i] or comps[i] == curdir:
483            del comps[i]
484        elif comps[i] == pardir:
485            if i > 0 and comps[i-1] != pardir:
486                del comps[i-1:i+1]
487                i -= 1
488            elif i == 0 and prefix.endswith(sep):
489                del comps[i]
490            else:
491                i += 1
492        else:
493            i += 1
494    # If the path is now empty, substitute '.'
495    if not prefix and not comps:
496        comps.append(curdir)
497    return prefix + sep.join(comps)
498
499def _abspath_fallback(path):
500    """Return the absolute version of a path as a fallback function in case
501    `nt._getfullpathname` is not available or raises OSError. See bpo-31047 for
502    more.
503
504    """
505
506    path = os.fspath(path)
507    if not isabs(path):
508        if isinstance(path, bytes):
509            cwd = os.getcwdb()
510        else:
511            cwd = os.getcwd()
512        path = join(cwd, path)
513    return normpath(path)
514
515# Return an absolute path.
516try:
517    from nt import _getfullpathname
518
519except ImportError: # not running on Windows - mock up something sensible
520    abspath = _abspath_fallback
521
522else:  # use native Windows method on Windows
523    def abspath(path):
524        """Return the absolute version of a path."""
525        try:
526            return normpath(_getfullpathname(path))
527        except (OSError, ValueError):
528            return _abspath_fallback(path)
529
530# realpath is a no-op on systems without islink support
531realpath = abspath
532# Win9x family and earlier have no Unicode filename support.
533supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and
534                              sys.getwindowsversion()[3] >= 2)
535
536def relpath(path, start=None):
537    """Return a relative version of a path"""
538    path = os.fspath(path)
539    if isinstance(path, bytes):
540        sep = b'\\'
541        curdir = b'.'
542        pardir = b'..'
543    else:
544        sep = '\\'
545        curdir = '.'
546        pardir = '..'
547
548    if start is None:
549        start = curdir
550
551    if not path:
552        raise ValueError("no path specified")
553
554    start = os.fspath(start)
555    try:
556        start_abs = abspath(normpath(start))
557        path_abs = abspath(normpath(path))
558        start_drive, start_rest = splitdrive(start_abs)
559        path_drive, path_rest = splitdrive(path_abs)
560        if normcase(start_drive) != normcase(path_drive):
561            raise ValueError("path is on mount %r, start on mount %r" % (
562                path_drive, start_drive))
563
564        start_list = [x for x in start_rest.split(sep) if x]
565        path_list = [x for x in path_rest.split(sep) if x]
566        # Work out how much of the filepath is shared by start and path.
567        i = 0
568        for e1, e2 in zip(start_list, path_list):
569            if normcase(e1) != normcase(e2):
570                break
571            i += 1
572
573        rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
574        if not rel_list:
575            return curdir
576        return join(*rel_list)
577    except (TypeError, ValueError, AttributeError, BytesWarning, DeprecationWarning):
578        genericpath._check_arg_types('relpath', path, start)
579        raise
580
581
582# Return the longest common sub-path of the sequence of paths given as input.
583# The function is case-insensitive and 'separator-insensitive', i.e. if the
584# only difference between two paths is the use of '\' versus '/' as separator,
585# they are deemed to be equal.
586#
587# However, the returned path will have the standard '\' separator (even if the
588# given paths had the alternative '/' separator) and will have the case of the
589# first path given in the sequence. Additionally, any trailing separator is
590# stripped from the returned path.
591
592def commonpath(paths):
593    """Given a sequence of path names, returns the longest common sub-path."""
594
595    if not paths:
596        raise ValueError('commonpath() arg is an empty sequence')
597
598    paths = tuple(map(os.fspath, paths))
599    if isinstance(paths[0], bytes):
600        sep = b'\\'
601        altsep = b'/'
602        curdir = b'.'
603    else:
604        sep = '\\'
605        altsep = '/'
606        curdir = '.'
607
608    try:
609        drivesplits = [splitdrive(p.replace(altsep, sep).lower()) for p in paths]
610        split_paths = [p.split(sep) for d, p in drivesplits]
611
612        try:
613            isabs, = set(p[:1] == sep for d, p in drivesplits)
614        except ValueError:
615            raise ValueError("Can't mix absolute and relative paths") from None
616
617        # Check that all drive letters or UNC paths match. The check is made only
618        # now otherwise type errors for mixing strings and bytes would not be
619        # caught.
620        if len(set(d for d, p in drivesplits)) != 1:
621            raise ValueError("Paths don't have the same drive")
622
623        drive, path = splitdrive(paths[0].replace(altsep, sep))
624        common = path.split(sep)
625        common = [c for c in common if c and c != curdir]
626
627        split_paths = [[c for c in s if c and c != curdir] for s in split_paths]
628        s1 = min(split_paths)
629        s2 = max(split_paths)
630        for i, c in enumerate(s1):
631            if c != s2[i]:
632                common = common[:i]
633                break
634        else:
635            common = common[:len(s1)]
636
637        prefix = drive + sep if isabs else drive
638        return prefix + sep.join(common)
639    except (TypeError, AttributeError):
640        genericpath._check_arg_types('commonpath', *paths)
641        raise
642
643
644# determine if two files are in fact the same file
645try:
646    # GetFinalPathNameByHandle is available starting with Windows 6.0.
647    # Windows XP and non-Windows OS'es will mock _getfinalpathname.
648    if sys.getwindowsversion()[:2] >= (6, 0):
649        from nt import _getfinalpathname
650    else:
651        raise ImportError
652except (AttributeError, ImportError):
653    # On Windows XP and earlier, two files are the same if their absolute
654    # pathnames are the same.
655    # Non-Windows operating systems fake this method with an XP
656    # approximation.
657    def _getfinalpathname(f):
658        return normcase(abspath(f))
659
660
661try:
662    # The genericpath.isdir implementation uses os.stat and checks the mode
663    # attribute to tell whether or not the path is a directory.
664    # This is overkill on Windows - just pass the path to GetFileAttributes
665    # and check the attribute from there.
666    from nt import _isdir as isdir
667except ImportError:
668    # Use genericpath.isdir as imported above.
669    pass
670