246 lines
7 KiB
Python
246 lines
7 KiB
Python
# 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, f"Failed getting a handle to: {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, f"Failed getting final path for: {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:])
|