Having trouble implementing a readlink() function

眉间皱痕 提交于 2019-11-28 12:14:38

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
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!