tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

path.py (7161B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 """
      6 Like :py:mod:`os.path`, with a reduced set of functions, and with normalized path
      7 separators (always use forward slashes).
      8 Also contains a few additional utilities not found in :py:mod:`os.path`.
      9 """
     10 
     11 import ctypes
     12 import os
     13 import posixpath
     14 import re
     15 import sys
     16 
     17 
     18 def normsep(path):
     19    """
     20    Normalize path separators, by using forward slashes instead of whatever
     21    :py:const:`os.sep` is.
     22    """
     23    if os.sep != "/":
     24        # Python 2 is happy to do things like byte_string.replace(u'foo',
     25        # u'bar'), but not Python 3.
     26        if isinstance(path, bytes):
     27            path = path.replace(os.sep.encode("ascii"), b"/")
     28        else:
     29            path = path.replace(os.sep, "/")
     30    if os.altsep and os.altsep != "/":
     31        if isinstance(path, bytes):
     32            path = path.replace(os.altsep.encode("ascii"), b"/")
     33        else:
     34            path = path.replace(os.altsep, "/")
     35    return path
     36 
     37 
     38 def cargo_workaround(path):
     39    unc = "//?/"
     40    if path.startswith(unc):
     41        return path[len(unc) :]
     42    return path
     43 
     44 
     45 def relpath(path, start):
     46    path = normsep(path)
     47    start = normsep(start)
     48    if sys.platform == "win32":
     49        # os.path.relpath can't handle relative paths between UNC and non-UNC
     50        # paths, so strip a //?/ prefix if present (bug 1581248)
     51        path = cargo_workaround(path)
     52        start = cargo_workaround(start)
     53    try:
     54        rel = os.path.relpath(path, start)
     55    except ValueError:
     56        # On Windows this can throw a ValueError if the two paths are on
     57        # different drives. In that case, just return the path.
     58        return abspath(path)
     59    rel = normsep(rel)
     60    return "" if rel == "." else rel
     61 
     62 
     63 def realpath(path):
     64    return normsep(os.path.realpath(path))
     65 
     66 
     67 def abspath(path):
     68    return normsep(os.path.abspath(path))
     69 
     70 
     71 def join(*paths):
     72    return normsep(os.path.join(*paths))
     73 
     74 
     75 def normpath(path):
     76    return posixpath.normpath(normsep(path))
     77 
     78 
     79 def dirname(path):
     80    return posixpath.dirname(normsep(path))
     81 
     82 
     83 def commonprefix(paths):
     84    return posixpath.commonprefix([normsep(path) for path in paths])
     85 
     86 
     87 def basename(path):
     88    return os.path.basename(path)
     89 
     90 
     91 def splitext(path):
     92    return posixpath.splitext(normsep(path))
     93 
     94 
     95 def split(path):
     96    """
     97    Return the normalized path as a list of its components.
     98 
     99        ``split('foo/bar/baz')`` returns ``['foo', 'bar', 'baz']``
    100    """
    101    return normsep(path).split("/")
    102 
    103 
    104 def basedir(path, bases):
    105    """
    106    Given a list of directories (`bases`), return which one contains the given
    107    path. If several matches are found, the deepest base directory is returned.
    108 
    109        ``basedir('foo/bar/baz', ['foo', 'baz', 'foo/bar'])`` returns ``'foo/bar'``
    110        (`'foo'` and `'foo/bar'` both match, but `'foo/bar'` is the deepest match)
    111    """
    112    path = normsep(path)
    113    bases = [normsep(b) for b in bases]
    114    if path in bases:
    115        return path
    116    for b in sorted(bases, reverse=True):
    117        if b == "" or path.startswith(b + "/"):
    118            return b
    119 
    120 
    121 re_cache = {}
    122 # Python versions < 3.7 return r'\/' for re.escape('/').
    123 if re.escape("/") == "/":
    124    MATCH_STAR_STAR_RE = re.compile(r"(^|/)\\\*\\\*/")
    125    MATCH_STAR_STAR_END_RE = re.compile(r"(^|/)\\\*\\\*$")
    126 else:
    127    MATCH_STAR_STAR_RE = re.compile(r"(^|\\\/)\\\*\\\*\\\/")
    128    MATCH_STAR_STAR_END_RE = re.compile(r"(^|\\\/)\\\*\\\*$")
    129 
    130 
    131 def match(path, pattern):
    132    """
    133    Return whether the given path matches the given pattern.
    134    An asterisk can be used to match any string, including the null string, in
    135    one part of the path:
    136 
    137        ``foo`` matches ``*``, ``f*`` or ``fo*o``
    138 
    139    However, an asterisk matching a subdirectory may not match the null string:
    140 
    141        ``foo/bar`` does *not* match ``foo/*/bar``
    142 
    143    If the pattern matches one of the ancestor directories of the path, the
    144    patch is considered matching:
    145 
    146        ``foo/bar`` matches ``foo``
    147 
    148    Two adjacent asterisks can be used to match files and zero or more
    149    directories and subdirectories.
    150 
    151        ``foo/bar`` matches ``foo/**/bar``, or ``**/bar``
    152    """
    153    if not pattern:
    154        return True
    155    if pattern not in re_cache:
    156        p = re.escape(pattern)
    157        p = MATCH_STAR_STAR_RE.sub(r"\1(?:.+/)?", p)
    158        p = MATCH_STAR_STAR_END_RE.sub(r"(?:\1.+)?", p)
    159        p = p.replace(r"\*", "[^/]*") + "(?:/.*)?$"
    160        re_cache[pattern] = re.compile(p)
    161    return re_cache[pattern].match(path) is not None
    162 
    163 
    164 def rebase(oldbase, base, relativepath):
    165    """
    166    Return `relativepath` relative to `base` instead of `oldbase`.
    167    """
    168    if base == oldbase:
    169        return relativepath
    170    if len(base) < len(oldbase):
    171        assert basedir(oldbase, [base]) == base
    172        relbase = relpath(oldbase, base)
    173        result = join(relbase, relativepath)
    174    else:
    175        assert basedir(base, [oldbase]) == oldbase
    176        relbase = relpath(base, oldbase)
    177        result = relpath(relativepath, relbase)
    178    result = normpath(result)
    179    if relativepath.endswith("/") and not result.endswith("/"):
    180        result += "/"
    181    return result
    182 
    183 
    184 def readlink(path):
    185    if hasattr(os, "readlink"):
    186        return normsep(os.readlink(path))
    187 
    188    # Unfortunately os.path.realpath doesn't support symlinks on Windows, and os.readlink
    189    # is only available on Windows with Python 3.2+. We have to resort to ctypes...
    190 
    191    assert sys.platform == "win32"
    192 
    193    CreateFileW = ctypes.windll.kernel32.CreateFileW
    194    CreateFileW.argtypes = [
    195        ctypes.wintypes.LPCWSTR,
    196        ctypes.wintypes.DWORD,
    197        ctypes.wintypes.DWORD,
    198        ctypes.wintypes.LPVOID,
    199        ctypes.wintypes.DWORD,
    200        ctypes.wintypes.DWORD,
    201        ctypes.wintypes.HANDLE,
    202    ]
    203    CreateFileW.restype = ctypes.wintypes.HANDLE
    204 
    205    GENERIC_READ = 0x80000000
    206    FILE_SHARE_READ = 0x00000001
    207    OPEN_EXISTING = 3
    208    FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
    209 
    210    handle = CreateFileW(
    211        path,
    212        GENERIC_READ,
    213        FILE_SHARE_READ,
    214        0,
    215        OPEN_EXISTING,
    216        FILE_FLAG_BACKUP_SEMANTICS,
    217        0,
    218    )
    219    assert handle != 1, f"Failed getting a handle to: {path}"
    220 
    221    MAX_PATH = 260
    222 
    223    buf = ctypes.create_unicode_buffer(MAX_PATH)
    224    GetFinalPathNameByHandleW = ctypes.windll.kernel32.GetFinalPathNameByHandleW
    225    GetFinalPathNameByHandleW.argtypes = [
    226        ctypes.wintypes.HANDLE,
    227        ctypes.wintypes.LPWSTR,
    228        ctypes.wintypes.DWORD,
    229        ctypes.wintypes.DWORD,
    230    ]
    231    GetFinalPathNameByHandleW.restype = ctypes.wintypes.DWORD
    232 
    233    FILE_NAME_NORMALIZED = 0x0
    234 
    235    rv = GetFinalPathNameByHandleW(handle, buf, MAX_PATH, FILE_NAME_NORMALIZED)
    236    assert rv != 0 and rv <= MAX_PATH, f"Failed getting final path for: {path}"
    237 
    238    CloseHandle = ctypes.windll.kernel32.CloseHandle
    239    CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
    240    CloseHandle.restype = ctypes.wintypes.BOOL
    241 
    242    rv = CloseHandle(handle)
    243    assert rv != 0, "Failed closing handle"
    244 
    245    # Remove leading '\\?\' from the result.
    246    return normsep(buf.value[4:])