Working with Windows Junctions in Python

· klm's blog


Original post is here: eklausmeier.goip.de

I had to detect Windows junctions (similar but not identical to symbolic links) in Python. On stackoverflow.com I read the following Python code given by a user named eryksun for handling them, as os.islink() does not work for junctions.

The routines access Windows functions CreateFileW(), DeviceIoControl(), CloseHandle() in kernel32.

[more_WP_Tag]

  1from ctypes import *
  2from ctypes.wintypes import *
  3
  4kernel32 = WinDLL('kernel32')
  5LPDWORD = POINTER(DWORD)
  6UCHAR = c_ubyte
  7
  8GetFileAttributesW = kernel32.GetFileAttributesW
  9GetFileAttributesW.restype = DWORD
 10GetFileAttributesW.argtypes = (LPCWSTR,) #lpFileName In
 11
 12INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
 13FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
 14
 15CreateFileW = kernel32.CreateFileW
 16CreateFileW.restype = HANDLE
 17CreateFileW.argtypes = (LPCWSTR, #lpFileName In
 18                        DWORD,   #dwDesiredAccess In
 19                        DWORD,   #dwShareMode In
 20                        LPVOID,  #lpSecurityAttributes In_opt
 21                        DWORD,   #dwCreationDisposition In
 22                        DWORD,   #dwFlagsAndAttributes In
 23                        HANDLE)  #hTemplateFile In_opt
 24
 25CloseHandle = kernel32.CloseHandle
 26CloseHandle.restype = BOOL
 27CloseHandle.argtypes = (HANDLE,) #hObject In
 28
 29INVALID_HANDLE_VALUE = HANDLE(-1).value
 30OPEN_EXISTING = 3
 31FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
 32FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
 33
 34DeviceIoControl = kernel32.DeviceIoControl
 35DeviceIoControl.restype = BOOL
 36DeviceIoControl.argtypes = (HANDLE,  #hDevice In
 37                            DWORD,   #dwIoControlCode In
 38                            LPVOID,  #lpInBuffer In_opt
 39                            DWORD,   #nInBufferSize In
 40                            LPVOID,  #lpOutBuffer Out_opt
 41                            DWORD,   #nOutBufferSize In
 42                            LPDWORD, #lpBytesReturned Out_opt
 43                            LPVOID)  #lpOverlapped Inout_opt
 44
 45FSCTL_GET_REPARSE_POINT = 0x000900A8
 46IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003
 47IO_REPARSE_TAG_SYMLINK = 0xA000000C
 48MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
 49
 50class GENERIC_REPARSE_BUFFER(Structure):
 51    _fields_ = (('DataBuffer', UCHAR * 1),)
 52
 53
 54class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
 55    _fields_ = (('SubstituteNameOffset', USHORT),
 56                ('SubstituteNameLength', USHORT),
 57                ('PrintNameOffset', USHORT),
 58                ('PrintNameLength', USHORT),
 59                ('Flags', ULONG),
 60                ('PathBuffer', WCHAR * 1))
 61    @property
 62    def PrintName(self):
 63        arrayt = WCHAR * (self.PrintNameLength // 2)
 64        offset = type(self).PathBuffer.offset + self.PrintNameOffset
 65        return arrayt.from_address(addressof(self) + offset).value
 66
 67
 68class MOUNT_POINT_REPARSE_BUFFER(Structure):
 69    _fields_ = (('SubstituteNameOffset', USHORT),
 70                ('SubstituteNameLength', USHORT),
 71                ('PrintNameOffset', USHORT),
 72                ('PrintNameLength', USHORT),
 73                ('PathBuffer', WCHAR * 1))
 74    @property
 75    def PrintName(self):
 76        arrayt = WCHAR * (self.PrintNameLength // 2)
 77        offset = type(self).PathBuffer.offset + self.PrintNameOffset
 78        return arrayt.from_address(addressof(self) + offset).value
 79
 80
 81class REPARSE_DATA_BUFFER(Structure):
 82    class REPARSE_BUFFER(Union):
 83        _fields_ = (('SymbolicLinkReparseBuffer',
 84                        SYMBOLIC_LINK_REPARSE_BUFFER),
 85                    ('MountPointReparseBuffer',
 86                        MOUNT_POINT_REPARSE_BUFFER),
 87                    ('GenericReparseBuffer',
 88                        GENERIC_REPARSE_BUFFER))
 89    _fields_ = (('ReparseTag', ULONG),
 90                ('ReparseDataLength', USHORT),
 91                ('Reserved', USHORT),
 92                ('ReparseBuffer', REPARSE_BUFFER))
 93    _anonymous_ = ('ReparseBuffer',)
 94
 95
 96def islink(path):
 97    result = GetFileAttributesW(path)
 98    if result == INVALID_FILE_ATTRIBUTES:
 99        raise WinError()
100    return bool(result & FILE_ATTRIBUTE_REPARSE_POINT)
101
102def readlink(path):
103    reparse_point_handle = CreateFileW(path,
104                                       0,
105                                       0,
106                                       None,
107                                       OPEN_EXISTING,
108                                       FILE_FLAG_OPEN_REPARSE_POINT |
109                                       FILE_FLAG_BACKUP_SEMANTICS,
110                                       None)
111    if reparse_point_handle == INVALID_HANDLE_VALUE:
112        raise WinError()
113    target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
114    n_bytes_returned = DWORD()
115    io_result = DeviceIoControl(reparse_point_handle,
116                                FSCTL_GET_REPARSE_POINT,
117                                None, 0,
118                                target_buffer, len(target_buffer),
119                                byref(n_bytes_returned),
120                                None)
121    CloseHandle(reparse_point_handle)
122    if not io_result:
123        raise WinError()
124    rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
125    if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
126        return rdb.SymbolicLinkReparseBuffer.PrintName
127    elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
128        return rdb.MountPointReparseBuffer.PrintName
129    raise ValueError("not a link")

The user calls islink() to detect whether the supplied path-name is a junction or not. readlink() gives you the path to which the arguments points to. So if X points to Y, then readlink(X) gives Y.

One comment in stackoverflow.com says: This works out of the box. Indeed.