Having trouble implementing a readlink() function

后端 未结 2 689
[愿得一人]
[愿得一人] 2020-12-11 04:26

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

相关标签:
2条回答
  • 2020-12-11 05:21

    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'
    
    0 讨论(0)
  • 2020-12-11 05:27

    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
    
    0 讨论(0)
提交回复
热议问题