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:])