Implement islink, readlink and realpath using Win32 api

Change-Id: I18452cbb32d24db73601ad10485dbe6bb278731c
This commit is contained in:
Renaud Paquay 2016-11-01 14:37:13 -07:00
parent 2a4be94878
commit 227ad2ef42
4 changed files with 227 additions and 19 deletions

View File

@ -242,3 +242,57 @@ def rename(src, dst):
raise raise
else: else:
os.rename(src, dst) os.rename(src, dst)
def islink(path):
"""Test whether a path is a symbolic link.
Availability: Windows, Unix.
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.islink(path)
else:
return os.path.islink(path)
def readlink(path):
"""Return a string representing the path to which the symbolic link
points. The result may be either an absolute or relative pathname;
if it is relative, it may be converted to an absolute pathname using
os.path.join(os.path.dirname(path), result).
Availability: Windows, Unix.
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.readlink(path)
else:
return os.readlink(path)
def realpath(path):
"""Return the canonical path of the specified filename, eliminating
any symbolic links encountered in the path.
Availability: Windows, Unix.
"""
if isWindows():
current_path = os.path.abspath(path)
path_tail = []
for c in range(0, 100): # Avoid cycles
if islink(current_path):
target = readlink(current_path)
current_path = os.path.join(os.path.dirname(current_path), target)
else:
basename = os.path.basename(current_path)
if basename == '':
path_tail.append(current_path)
break
path_tail.append(basename)
current_path = os.path.dirname(current_path)
path_tail.reverse()
result = os.path.normpath(os.path.join(*path_tail))
return result
else:
return os.path.realpath(path)

View File

@ -15,13 +15,20 @@
import errno import errno
from ctypes import WinDLL, get_last_error, FormatError, WinError from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof
from ctypes.wintypes import BOOL, LPCWSTR, DWORD from ctypes import c_buffer
from ctypes.wintypes import BOOL, LPCWSTR, DWORD, HANDLE, POINTER, c_ubyte
from ctypes.wintypes import WCHAR, USHORT, LPVOID, Structure, Union, ULONG
from ctypes.wintypes import byref
kernel32 = WinDLL('kernel32', use_last_error=True) kernel32 = WinDLL('kernel32', use_last_error=True)
LPDWORD = POINTER(DWORD)
UCHAR = c_ubyte
# Win32 error codes # Win32 error codes
ERROR_SUCCESS = 0 ERROR_SUCCESS = 0
ERROR_NOT_SUPPORTED = 50
ERROR_PRIVILEGE_NOT_HELD = 1314 ERROR_PRIVILEGE_NOT_HELD = 1314
# Win32 API entry points # Win32 API entry points
@ -35,6 +42,94 @@ CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In
SYMBOLIC_LINK_FLAG_FILE = 0x00 SYMBOLIC_LINK_FLAG_FILE = 0x00
SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01 SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01
GetFileAttributesW = kernel32.GetFileAttributesW
GetFileAttributesW.restype = DWORD
GetFileAttributesW.argtypes = (LPCWSTR,) # lpFileName In
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
CreateFileW = kernel32.CreateFileW
CreateFileW.restype = HANDLE
CreateFileW.argtypes = (LPCWSTR, # lpFileName In
DWORD, # dwDesiredAccess In
DWORD, # dwShareMode In
LPVOID, # lpSecurityAttributes In_opt
DWORD, # dwCreationDisposition In
DWORD, # dwFlagsAndAttributes In
HANDLE) # hTemplateFile In_opt
CloseHandle = kernel32.CloseHandle
CloseHandle.restype = BOOL
CloseHandle.argtypes = (HANDLE,) # hObject In
INVALID_HANDLE_VALUE = HANDLE(-1).value
OPEN_EXISTING = 3
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
DeviceIoControl = kernel32.DeviceIoControl
DeviceIoControl.restype = BOOL
DeviceIoControl.argtypes = (HANDLE, # hDevice In
DWORD, # dwIoControlCode In
LPVOID, # lpInBuffer In_opt
DWORD, # nInBufferSize In
LPVOID, # lpOutBuffer Out_opt
DWORD, # nOutBufferSize In
LPDWORD, # lpBytesReturned Out_opt
LPVOID) # lpOverlapped Inout_opt
# Device I/O control flags and options
FSCTL_GET_REPARSE_POINT = 0x000900A8
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
IO_REPARSE_TAG_SYMLINK = 0xA000000C
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
class GENERIC_REPARSE_BUFFER(Structure):
_fields_ = (('DataBuffer', UCHAR * 1),)
class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('Flags', ULONG),
('PathBuffer', WCHAR * 1))
@property
def PrintName(self):
arrayt = WCHAR * (self.PrintNameLength // 2)
offset = type(self).PathBuffer.offset + self.PrintNameOffset
return arrayt.from_address(addressof(self) + offset).value
class MOUNT_POINT_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('PathBuffer', WCHAR * 1))
@property
def PrintName(self):
arrayt = WCHAR * (self.PrintNameLength // 2)
offset = type(self).PathBuffer.offset + self.PrintNameOffset
return arrayt.from_address(addressof(self) + offset).value
class REPARSE_DATA_BUFFER(Structure):
class REPARSE_BUFFER(Union):
_fields_ = (('SymbolicLinkReparseBuffer', SYMBOLIC_LINK_REPARSE_BUFFER),
('MountPointReparseBuffer', MOUNT_POINT_REPARSE_BUFFER),
('GenericReparseBuffer', GENERIC_REPARSE_BUFFER))
_fields_ = (('ReparseTag', ULONG),
('ReparseDataLength', USHORT),
('Reserved', USHORT),
('ReparseBuffer', REPARSE_BUFFER))
_anonymous_ = ('ReparseBuffer',)
def create_filesymlink(source, link_name): def create_filesymlink(source, link_name):
"""Creates a Windows file symbolic link source pointing to link_name.""" """Creates a Windows file symbolic link source pointing to link_name."""
@ -58,6 +153,65 @@ def _create_symlink(source, link_name, dwFlags):
error_desc = FormatError(code).strip() error_desc = FormatError(code).strip()
if code == ERROR_PRIVILEGE_NOT_HELD: if code == ERROR_PRIVILEGE_NOT_HELD:
raise OSError(errno.EPERM, error_desc, link_name) raise OSError(errno.EPERM, error_desc, link_name)
error_desc = 'Error creating symbolic link %s: %s'.format( _raise_winerror(
link_name, error_desc) code,
'Error creating symbolic link \"%s\"'.format(link_name))
def islink(path):
result = GetFileAttributesW(path)
if result == INVALID_FILE_ATTRIBUTES:
return False
return bool(result & FILE_ATTRIBUTE_REPARSE_POINT)
def readlink(path):
reparse_point_handle = CreateFileW(path,
0,
0,
None,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT |
FILE_FLAG_BACKUP_SEMANTICS,
None)
if reparse_point_handle == INVALID_HANDLE_VALUE:
_raise_winerror(
get_last_error(),
'Error opening symblic link \"%s\"'.format(path))
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
n_bytes_returned = DWORD()
io_result = DeviceIoControl(reparse_point_handle,
FSCTL_GET_REPARSE_POINT,
None,
0,
target_buffer,
len(target_buffer),
byref(n_bytes_returned),
None)
CloseHandle(reparse_point_handle)
if not io_result:
_raise_winerror(
get_last_error(),
'Error reading symblic link \"%s\"'.format(path))
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
return _preserve_encoding(path, rdb.SymbolicLinkReparseBuffer.PrintName)
elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
return _preserve_encoding(path, rdb.MountPointReparseBuffer.PrintName)
# Unsupported reparse point type
_raise_winerror(
ERROR_NOT_SUPPORTED,
'Error reading symblic link \"%s\"'.format(path))
def _preserve_encoding(source, target):
"""Ensures target is the same string type (i.e. unicode or str) as source."""
if isinstance(source, unicode):
return unicode(target)
return str(target)
def _raise_winerror(code, error_desc):
win_error_desc = FormatError(code).strip()
error_desc = "%s: %s".format(error_desc, win_error_desc)
raise WinError(code, error_desc) raise WinError(code, error_desc)

View File

@ -103,7 +103,7 @@ def _ProjectHooks():
""" """
global _project_hook_list global _project_hook_list
if _project_hook_list is None: if _project_hook_list is None:
d = os.path.realpath(os.path.abspath(os.path.dirname(__file__))) d = platform_utils.realpath(os.path.abspath(os.path.dirname(__file__)))
d = os.path.join(d, 'hooks') d = os.path.join(d, 'hooks')
_project_hook_list = [os.path.join(d, x) for x in os.listdir(d)] _project_hook_list = [os.path.join(d, x) for x in os.listdir(d)]
return _project_hook_list return _project_hook_list
@ -275,7 +275,7 @@ class _LinkFile(object):
def __linkIt(self, relSrc, absDest): def __linkIt(self, relSrc, absDest):
# link file if it does not exist or is out of date # link file if it does not exist or is out of date
if not os.path.islink(absDest) or (os.readlink(absDest) != relSrc): if not platform_utils.islink(absDest) or (platform_utils.readlink(absDest) != relSrc):
try: try:
# remove existing file first, since it might be read-only # remove existing file first, since it might be read-only
if os.path.lexists(absDest): if os.path.lexists(absDest):
@ -2315,10 +2315,10 @@ class Project(object):
print("Retrying clone after deleting %s" % print("Retrying clone after deleting %s" %
self.gitdir, file=sys.stderr) self.gitdir, file=sys.stderr)
try: try:
platform_utils.rmtree(os.path.realpath(self.gitdir)) platform_utils.rmtree(platform_utils.realpath(self.gitdir))
if self.worktree and os.path.exists(os.path.realpath if self.worktree and os.path.exists(platform_utils.realpath
(self.worktree)): (self.worktree)):
platform_utils.rmtree(os.path.realpath(self.worktree)) platform_utils.rmtree(platform_utils.realpath(self.worktree))
return self._InitGitDir(mirror_git=mirror_git, force_sync=False) return self._InitGitDir(mirror_git=mirror_git, force_sync=False)
except: except:
raise e raise e
@ -2370,7 +2370,7 @@ class Project(object):
self._InitHooks() self._InitHooks()
def _InitHooks(self): def _InitHooks(self):
hooks = os.path.realpath(self._gitdir_path('hooks')) hooks = platform_utils.realpath(self._gitdir_path('hooks'))
if not os.path.exists(hooks): if not os.path.exists(hooks):
os.makedirs(hooks) os.makedirs(hooks)
for stock_hook in _ProjectHooks(): for stock_hook in _ProjectHooks():
@ -2386,7 +2386,7 @@ class Project(object):
continue continue
dst = os.path.join(hooks, name) dst = os.path.join(hooks, name)
if os.path.islink(dst): if platform_utils.islink(dst):
continue continue
if os.path.exists(dst): if os.path.exists(dst):
if filecmp.cmp(stock_hook, dst, shallow=False): if filecmp.cmp(stock_hook, dst, shallow=False):
@ -2448,9 +2448,9 @@ class Project(object):
symlink_dirs += self.working_tree_dirs symlink_dirs += self.working_tree_dirs
to_symlink = symlink_files + symlink_dirs to_symlink = symlink_files + symlink_dirs
for name in set(to_symlink): for name in set(to_symlink):
dst = os.path.realpath(os.path.join(destdir, name)) dst = platform_utils.realpath(os.path.join(destdir, name))
if os.path.lexists(dst): if os.path.lexists(dst):
src = os.path.realpath(os.path.join(srcdir, name)) src = platform_utils.realpath(os.path.join(srcdir, name))
# Fail if the links are pointing to the wrong place # Fail if the links are pointing to the wrong place
if src != dst: if src != dst:
_error('%s is different in %s vs %s', name, destdir, srcdir) _error('%s is different in %s vs %s', name, destdir, srcdir)
@ -2482,10 +2482,10 @@ class Project(object):
if copy_all: if copy_all:
to_copy = os.listdir(gitdir) to_copy = os.listdir(gitdir)
dotgit = os.path.realpath(dotgit) dotgit = platform_utils.realpath(dotgit)
for name in set(to_copy).union(to_symlink): for name in set(to_copy).union(to_symlink):
try: try:
src = os.path.realpath(os.path.join(gitdir, name)) src = platform_utils.realpath(os.path.join(gitdir, name))
dst = os.path.join(dotgit, name) dst = os.path.join(dotgit, name)
if os.path.lexists(dst): if os.path.lexists(dst):
@ -2498,7 +2498,7 @@ class Project(object):
if name in to_symlink: if name in to_symlink:
platform_utils.symlink( platform_utils.symlink(
os.path.relpath(src, os.path.dirname(dst)), dst) os.path.relpath(src, os.path.dirname(dst)), dst)
elif copy_all and not os.path.islink(dst): elif copy_all and not platform_utils.islink(dst):
if os.path.isdir(src): if os.path.isdir(src):
shutil.copytree(src, dst) shutil.copytree(src, dst)
elif os.path.isfile(src): elif os.path.isfile(src):
@ -2556,7 +2556,7 @@ class Project(object):
raise raise
def _gitdir_path(self, path): def _gitdir_path(self, path):
return os.path.realpath(os.path.join(self.gitdir, path)) return platform_utils.realpath(os.path.join(self.gitdir, path))
def _revlist(self, *args, **kw): def _revlist(self, *args, **kw):
a = [] a = []

View File

@ -498,7 +498,7 @@ later is required to fix a server side protocol bug.
dirs_to_remove += [os.path.join(root, d) for d in dirs dirs_to_remove += [os.path.join(root, d) for d in dirs
if os.path.join(root, d) not in dirs_to_remove] if os.path.join(root, d) not in dirs_to_remove]
for d in reversed(dirs_to_remove): for d in reversed(dirs_to_remove):
if os.path.islink(d): if platform_utils.islink(d):
try: try:
os.remove(d) os.remove(d)
except OSError: except OSError: