# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Like :py:mod:`os.path`, with a reduced set of functions, and with normalized path separators (always use forward slashes). Also contains a few additional utilities not found in :py:mod:`os.path`. """ import ctypes import os import posixpath import re import sys def normsep(path): """ Normalize path separators, by using forward slashes instead of whatever :py:const:`os.sep` is. """ if os.sep != "/": # Python 2 is happy to do things like byte_string.replace(u'foo', # u'bar'), but not Python 3. if isinstance(path, bytes): path = path.replace(os.sep.encode("ascii"), b"/") else: path = path.replace(os.sep, "/") if os.altsep and os.altsep != "/": if isinstance(path, bytes): path = path.replace(os.altsep.encode("ascii"), b"/") else: path = path.replace(os.altsep, "/") return path def cargo_workaround(path): unc = "//?/" if path.startswith(unc): return path[len(unc) :] return path def relpath(path, start): path = normsep(path) start = normsep(start) if sys.platform == "win32": # os.path.relpath can't handle relative paths between UNC and non-UNC # paths, so strip a //?/ prefix if present (bug 1581248) path = cargo_workaround(path) start = cargo_workaround(start) try: rel = os.path.relpath(path, start) except ValueError: # On Windows this can throw a ValueError if the two paths are on # different drives. In that case, just return the path. return abspath(path) rel = normsep(rel) return "" if rel == "." else rel def realpath(path): return normsep(os.path.realpath(path)) def abspath(path): return normsep(os.path.abspath(path)) def join(*paths): return normsep(os.path.join(*paths)) def normpath(path): return posixpath.normpath(normsep(path)) def dirname(path): return posixpath.dirname(normsep(path)) def commonprefix(paths): return posixpath.commonprefix([normsep(path) for path in paths]) def basename(path): return os.path.basename(path) def splitext(path): return posixpath.splitext(normsep(path)) def split(path): """ Return the normalized path as a list of its components. ``split('foo/bar/baz')`` returns ``['foo', 'bar', 'baz']`` """ return normsep(path).split("/") def basedir(path, bases): """ Given a list of directories (`bases`), return which one contains the given path. If several matches are found, the deepest base directory is returned. ``basedir('foo/bar/baz', ['foo', 'baz', 'foo/bar'])`` returns ``'foo/bar'`` (`'foo'` and `'foo/bar'` both match, but `'foo/bar'` is the deepest match) """ path = normsep(path) bases = [normsep(b) for b in bases] if path in bases: return path for b in sorted(bases, reverse=True): if b == "" or path.startswith(b + "/"): return b re_cache = {} # Python versions < 3.7 return r'\/' for re.escape('/'). if re.escape("/") == "/": MATCH_STAR_STAR_RE = re.compile(r"(^|/)\\\*\\\*/") MATCH_STAR_STAR_END_RE = re.compile(r"(^|/)\\\*\\\*$") else: MATCH_STAR_STAR_RE = re.compile(r"(^|\\\/)\\\*\\\*\\\/") MATCH_STAR_STAR_END_RE = re.compile(r"(^|\\\/)\\\*\\\*$") def match(path, pattern): """ Return whether the given path matches the given pattern. An asterisk can be used to match any string, including the null string, in one part of the path: ``foo`` matches ``*``, ``f*`` or ``fo*o`` However, an asterisk matching a subdirectory may not match the null string: ``foo/bar`` does *not* match ``foo/*/bar`` If the pattern matches one of the ancestor directories of the path, the patch is considered matching: ``foo/bar`` matches ``foo`` Two adjacent asterisks can be used to match files and zero or more directories and subdirectories. ``foo/bar`` matches ``foo/**/bar``, or ``**/bar`` """ if not pattern: return True if pattern not in re_cache: p = re.escape(pattern) p = MATCH_STAR_STAR_RE.sub(r"\1(?:.+/)?", p) p = MATCH_STAR_STAR_END_RE.sub(r"(?:\1.+)?", p) p = p.replace(r"\*", "[^/]*") + "(?:/.*)?$" re_cache[pattern] = re.compile(p) return re_cache[pattern].match(path) is not None def rebase(oldbase, base, relativepath): """ Return `relativepath` relative to `base` instead of `oldbase`. """ if base == oldbase: return relativepath if len(base) < len(oldbase): assert basedir(oldbase, [base]) == base relbase = relpath(oldbase, base) result = join(relbase, relativepath) else: assert basedir(base, [oldbase]) == oldbase relbase = relpath(base, oldbase) result = relpath(relativepath, relbase) result = normpath(result) if relativepath.endswith("/") and not result.endswith("/"): result += "/" return result def readlink(path): if hasattr(os, "readlink"): return normsep(os.readlink(path)) # Unfortunately os.path.realpath doesn't support symlinks on Windows, and os.readlink # is only available on Windows with Python 3.2+. We have to resort to ctypes... assert sys.platform == "win32" CreateFileW = ctypes.windll.kernel32.CreateFileW CreateFileW.argtypes = [ ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.LPVOID, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.HANDLE, ] CreateFileW.restype = ctypes.wintypes.HANDLE GENERIC_READ = 0x80000000 FILE_SHARE_READ = 0x00000001 OPEN_EXISTING = 3 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 handle = CreateFileW( path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0, ) assert handle != 1, "Failed getting a handle to: {}".format(path) MAX_PATH = 260 buf = ctypes.create_unicode_buffer(MAX_PATH) GetFinalPathNameByHandleW = ctypes.windll.kernel32.GetFinalPathNameByHandleW GetFinalPathNameByHandleW.argtypes = [ ctypes.wintypes.HANDLE, ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ] GetFinalPathNameByHandleW.restype = ctypes.wintypes.DWORD FILE_NAME_NORMALIZED = 0x0 rv = GetFinalPathNameByHandleW(handle, buf, MAX_PATH, FILE_NAME_NORMALIZED) assert rv != 0 and rv <= MAX_PATH, "Failed getting final path for: {}".format(path) CloseHandle = ctypes.windll.kernel32.CloseHandle CloseHandle.argtypes = [ctypes.wintypes.HANDLE] CloseHandle.restype = ctypes.wintypes.BOOL rv = CloseHandle(handle) assert rv != 0, "Failed closing handle" # Remove leading '\\?\' from the result. return normsep(buf.value[4:])