I\'ve been trying to figure out a way to get some sort of ability to be able to return the true abspath of a symbolic link in Windows, under Python 2.7. (I cannot upgrade to
ERROR_MOD_NOT_FOUND
(126) is likely due to windll.CloseHandle(hfile)
, which tries to load "closehandle.dll". It's missing kernel32
.
Here's an alternate implementation that handles junctions as well as symbolic links.
ctypes definitions
import ctypes
from ctypes import wintypes
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
FILE_READ_ATTRIBUTES = 0x0080
OPEN_EXISTING = 3
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
FILE_ATTRIBUTE_REPARSE_POINT = 0x0400
IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
IO_REPARSE_TAG_SYMLINK = 0xA000000C
FSCTL_GET_REPARSE_POINT = 0x000900A8
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
LPDWORD = ctypes.POINTER(wintypes.DWORD)
LPWIN32_FIND_DATA = ctypes.POINTER(wintypes.WIN32_FIND_DATAW)
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
def IsReparseTagNameSurrogate(tag):
return bool(tag & 0x20000000)
def _check_invalid_handle(result, func, args):
if result == INVALID_HANDLE_VALUE:
raise ctypes.WinError(ctypes.get_last_error())
return args
def _check_bool(result, func, args):
if not result:
raise ctypes.WinError(ctypes.get_last_error())
return args
kernel32.FindFirstFileW.errcheck = _check_invalid_handle
kernel32.FindFirstFileW.restype = wintypes.HANDLE
kernel32.FindFirstFileW.argtypes = (
wintypes.LPCWSTR, # _In_ lpFileName
LPWIN32_FIND_DATA) # _Out_ lpFindFileData
kernel32.FindClose.argtypes = (
wintypes.HANDLE,) # _Inout_ hFindFile
kernel32.CreateFileW.errcheck = _check_invalid_handle
kernel32.CreateFileW.restype = wintypes.HANDLE
kernel32.CreateFileW.argtypes = (
wintypes.LPCWSTR, # _In_ lpFileName
wintypes.DWORD, # _In_ dwDesiredAccess
wintypes.DWORD, # _In_ dwShareMode
wintypes.LPVOID, # _In_opt_ lpSecurityAttributes
wintypes.DWORD, # _In_ dwCreationDisposition
wintypes.DWORD, # _In_ dwFlagsAndAttributes
wintypes.HANDLE) # _In_opt_ hTemplateFile
kernel32.CloseHandle.argtypes = (
wintypes.HANDLE,) # _In_ hObject
kernel32.DeviceIoControl.errcheck = _check_bool
kernel32.DeviceIoControl.argtypes = (
wintypes.HANDLE, # _In_ hDevice
wintypes.DWORD, # _In_ dwIoControlCode
wintypes.LPVOID, # _In_opt_ lpInBuffer
wintypes.DWORD, # _In_ nInBufferSize
wintypes.LPVOID, # _Out_opt_ lpOutBuffer
wintypes.DWORD, # _In_ nOutBufferSize
LPDWORD, # _Out_opt_ lpBytesReturned
wintypes.LPVOID) # _Inout_opt_ lpOverlapped
class REPARSE_DATA_BUFFER(ctypes.Structure):
class ReparseData(ctypes.Union):
class LinkData(ctypes.Structure):
_fields_ = (('SubstituteNameOffset', wintypes.USHORT),
('SubstituteNameLength', wintypes.USHORT),
('PrintNameOffset', wintypes.USHORT),
('PrintNameLength', wintypes.USHORT))
@property
def PrintName(self):
dt = wintypes.WCHAR * (self.PrintNameLength //
ctypes.sizeof(wintypes.WCHAR))
name = dt.from_address(ctypes.addressof(self.PathBuffer) +
self.PrintNameOffset).value
if name.startswith(r'\??'):
name = r'\\?' + name[3:] # NT => Windows
return name
class SymbolicLinkData(LinkData):
_fields_ = (('Flags', wintypes.ULONG),
('PathBuffer', wintypes.BYTE * 0))
class MountPointData(LinkData):
_fields_ = (('PathBuffer', wintypes.BYTE * 0),)
class GenericData(ctypes.Structure):
_fields_ = (('DataBuffer', wintypes.BYTE * 0),)
_fields_ = (('SymbolicLinkReparseBuffer', SymbolicLinkData),
('MountPointReparseBuffer', MountPointData),
('GenericReparseBuffer', GenericData))
_fields_ = (('ReparseTag', wintypes.ULONG),
('ReparseDataLength', wintypes.USHORT),
('Reserved', wintypes.USHORT),
('ReparseData', ReparseData))
_anonymous_ = ('ReparseData',)
functions
def islink(path):
data = wintypes.WIN32_FIND_DATAW()
kernel32.FindClose(kernel32.FindFirstFileW(path, ctypes.byref(data)))
if not data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT:
return False
return IsReparseTagNameSurrogate(data.dwReserved0)
def readlink(path):
n = wintypes.DWORD()
buf = (wintypes.BYTE * MAXIMUM_REPARSE_DATA_BUFFER_SIZE)()
flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS
handle = kernel32.CreateFileW(path, FILE_READ_ATTRIBUTES, 0, None,
OPEN_EXISTING, flags, None)
try:
kernel32.DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, None, 0,
buf, ctypes.sizeof(buf), ctypes.byref(n), None)
finally:
kernel32.CloseHandle(handle)
rb = REPARSE_DATA_BUFFER.from_buffer(buf)
tag = rb.ReparseTag
if tag == IO_REPARSE_TAG_SYMLINK:
return rb.SymbolicLinkReparseBuffer.PrintName
if tag == IO_REPARSE_TAG_MOUNT_POINT:
return rb.MountPointReparseBuffer.PrintName
if not IsReparseTagNameSurrogate(tag):
raise ValueError("not a link")
raise ValueError("unsupported reparse tag: %d" % tag)
example
>>> sys.version
'2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:53:40)
[MSC v.1500 64 bit (AMD64)]'
>>> os.system(r'mklink /d spam C:\Windows')
symbolic link created for spam <<===>> C:\Windows
0
>>> islink('spam')
True
>>> readlink('spam')
u'C:\\Windows'
>>> islink('C:/Documents and Settings') # junction
True
>>> readlink('C:/Documents and Settings')
u'C:\\Users'
>>> islink('C:/Users/All Users') # symlinkd
True
>>> readlink('C:/Users/All Users')
u'C:\\ProgramData'
This is implemented in Tcl as file readlink
and the implementation of this might be worth reading as there seem to be a few differences. The WinReadLinkDirectory function calls NativeReadReparse to read the REPARSE_DATA_BUFFER
but this uses different flags to the CreateFile
function. Also the buffer size is different I think and the size of structures in Win32 calls is often used to detect the version of the API used so it is likely worth taking care to set the size to the correct value (or possibly the same value used in the Tcl implementation).
Just to show what I mean by Tcl support for this:
C:\>dir
Directory of C:\
22/09/2014 14:29 <JUNCTION> Code [C:\src]
...
C:\>tclsh
% file readlink Code
C:\src