From 227ad2ef42f47798d24814dfc2cef8119c313ab7 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Tue, 1 Nov 2016 14:37:13 -0700 Subject: [PATCH] Implement islink, readlink and realpath using Win32 api Change-Id: I18452cbb32d24db73601ad10485dbe6bb278731c --- platform_utils.py | 54 +++++++++++++ platform_utils_win32.py | 164 ++++++++++++++++++++++++++++++++++++++-- project.py | 26 +++---- subcmds/sync.py | 2 +- 4 files changed, 227 insertions(+), 19 deletions(-) diff --git a/platform_utils.py b/platform_utils.py index e0fa9dcc..2ad56490 100644 --- a/platform_utils.py +++ b/platform_utils.py @@ -242,3 +242,57 @@ def rename(src, dst): raise else: 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) diff --git a/platform_utils_win32.py b/platform_utils_win32.py index 02fb013a..fe76b3d6 100644 --- a/platform_utils_win32.py +++ b/platform_utils_win32.py @@ -15,13 +15,20 @@ import errno -from ctypes import WinDLL, get_last_error, FormatError, WinError -from ctypes.wintypes import BOOL, LPCWSTR, DWORD +from ctypes import WinDLL, get_last_error, FormatError, WinError, addressof +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) +LPDWORD = POINTER(DWORD) +UCHAR = c_ubyte + # Win32 error codes ERROR_SUCCESS = 0 +ERROR_NOT_SUPPORTED = 50 ERROR_PRIVILEGE_NOT_HELD = 1314 # Win32 API entry points @@ -35,6 +42,94 @@ CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In SYMBOLIC_LINK_FLAG_FILE = 0x00 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): """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() if code == ERROR_PRIVILEGE_NOT_HELD: raise OSError(errno.EPERM, error_desc, link_name) - error_desc = 'Error creating symbolic link %s: %s'.format( - link_name, error_desc) - raise WinError(code, error_desc) + _raise_winerror( + 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) diff --git a/project.py b/project.py index d4c5afd5..655b2024 100644 --- a/project.py +++ b/project.py @@ -103,7 +103,7 @@ def _ProjectHooks(): """ global _project_hook_list 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') _project_hook_list = [os.path.join(d, x) for x in os.listdir(d)] return _project_hook_list @@ -275,7 +275,7 @@ class _LinkFile(object): def __linkIt(self, relSrc, absDest): # 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: # remove existing file first, since it might be read-only if os.path.lexists(absDest): @@ -2315,10 +2315,10 @@ class Project(object): print("Retrying clone after deleting %s" % self.gitdir, file=sys.stderr) try: - platform_utils.rmtree(os.path.realpath(self.gitdir)) - if self.worktree and os.path.exists(os.path.realpath + platform_utils.rmtree(platform_utils.realpath(self.gitdir)) + if self.worktree and os.path.exists(platform_utils.realpath (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) except: raise e @@ -2370,7 +2370,7 @@ class Project(object): self._InitHooks() 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): os.makedirs(hooks) for stock_hook in _ProjectHooks(): @@ -2386,7 +2386,7 @@ class Project(object): continue dst = os.path.join(hooks, name) - if os.path.islink(dst): + if platform_utils.islink(dst): continue if os.path.exists(dst): if filecmp.cmp(stock_hook, dst, shallow=False): @@ -2448,9 +2448,9 @@ class Project(object): symlink_dirs += self.working_tree_dirs to_symlink = symlink_files + symlink_dirs 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): - 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 if src != dst: _error('%s is different in %s vs %s', name, destdir, srcdir) @@ -2482,10 +2482,10 @@ class Project(object): if copy_all: to_copy = os.listdir(gitdir) - dotgit = os.path.realpath(dotgit) + dotgit = platform_utils.realpath(dotgit) for name in set(to_copy).union(to_symlink): 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) if os.path.lexists(dst): @@ -2498,7 +2498,7 @@ class Project(object): if name in to_symlink: platform_utils.symlink( 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): shutil.copytree(src, dst) elif os.path.isfile(src): @@ -2556,7 +2556,7 @@ class Project(object): raise 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): a = [] diff --git a/subcmds/sync.py b/subcmds/sync.py index b88c596d..93fea23b 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -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 if os.path.join(root, d) not in dirs_to_remove] for d in reversed(dirs_to_remove): - if os.path.islink(d): + if platform_utils.islink(d): try: os.remove(d) except OSError: