pythonwindowsshellctypesactivepython

How to use SHGetFileInfo with SHGFI_PIDL in python


I'm trying to retrieve file information (specifically info about the icon) using SHGetFileInfo. In reality, I don't have the full path of the file, I only have the pidl.

The following code returns (0L, (0, 0, 0, '', '')) and my question is why.

from win32com.shell import shell, shellcon
def get_info():
    desktop = shell.SHGetDesktopFolder()
    eaten, desktop_pidl, attr = desktop.ParseDisplayName(None, None, r"C:\Users\Ella\Desktop")
    return shell.SHGetFileInfo(desktop_pidl, 0, shellcon.SHGFI_PIDL | shellcon.SHGFI_SYSICONINDEX | shellcon.SHGFI_ICON | shellcon.SHGFI_DISPLAYNAME)

On the other hand, the code bellow does work for some reason (it uses full path instead of pidl):

from win32com.shell import shell, shellcon
def get_info2():
    return shell.SHGetFileInfo(r"C:\Users\Ella\Desktop", 0, shellcon.SHGFI_SYSICONINDEX | shellcon.SHGFI_ICON | shellcon.SHGFI_DISPLAYNAME)

Thanks!


Solution

  • You've uncovered a bug in PySHGetFileInfo. If SHGFI_PIDL is set in flags, it calls PyObject_AsPIDL and stores the result to pidl_or_name, but it mistakenly passes name to SHGetFileInfo, which in this case is the initial NULL value. See below for more details.

    You asked how to set a breakpoint on shell32!SHGetFileInfoW. There's no simple answer to that. Instead allow me to share an overview of what I did to test this. Hopefully this will at least get you started.

    Test environment:

    Set up the shell environment.

    "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.Cmd"
    set MSSdk=%WindowsSDKDir%
    set SYMDIR=C:\Symbols
    set SYMSRV=http://msdl.microsoft.com/download/symbols    
    set _NT_SYMBOL_PATH=symsrv*symsrv.dll*%SYMDIR%*%SYMSRV%
    path C:\Program Files\Debugging Tools for Windows (x64);%PATH%
    path C:\Program Files\Mercurial;%PATH%
    

    Create a Python virtual environment.

    py -3.4 -m venv --symlinks test
    

    venv doesn't link the .pdb files, so grab those manually in a for loop.

    set PYDIR="%ProgramW6432%\Python34"
    set CMD=mklink "test\Scripts\%~nxf" "%f"
    for /R %PYDIR% %f in (*.pdb) do @%CMD%
    

    Activate the virtual environment.

    test\Scripts\activate
    

    Clone the PyWin32 repo. Build and install version 219.

    set HGSRV=http://pywin32.hg.sourceforge.net
    hg clone %HGSRV%/hgroot/pywin32/pywin32
    cd pywin32
    hg up b219
    

    I edited setup.py to comment out everything related to building win32com.mapi. My setup didn't even have the required headers, and when I obtained them there were problems building the extension for WIN64.

    Build and install the package.

    python setup3.py install
    

    Run Python under the console debugger, cdb.exe.

    >cdb -xi ld python
    
    Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
    Copyright (c) Microsoft Corporation. All rights reserved.
    
    CommandLine: python
    Symbol search path is: symsrv*symsrv.dll*C:\Symbols*
                           http://msdl.microsoft.com/download/symbols
    Executable search path is:
    (d50.1174): Break instruction exception - code 80000003 (first chance)
    ntdll!LdrpDoDebuggerBreak+0x30:
    00000000`770bcb70 cc              int     3
    0:000> bp shell32!SHGetFileInfoW
    0:000> g
    Python 3.4.2 (v3.4.2:ab2c023a9432, Oct  6 2014, 22:16:31)
    [MSC v.1600 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    

    The option -xi ld in the above command line sets a filter to ignore printing loaded modules. There are lots of tutorials and 'cheat sheets' online for using Microsoft's debuggers such as WinDbg, cdb, and kd. The debuggers all use the same engine, so they support a common set of debugging commands.

    The attached debugger has a breakpoint set on shell32!SHGetFileInfoW. When the breakpoint is triggered, the debugger grabs the console. One of the few redeeming features of the Windows console is its per-application input history and aliases. This makes it convenient to recall commands when bouncing in and out of the debugger and debuggee in the same console window.

    >>> import os
    >>> from win32com.shell import shell, shellcon
    >>> print(shell.__file__)
    C:\Temp\test\lib\site-packages\win32comext\shell\shell.pyd
    >>> path = os.path.expanduser(r'~\Desktop\desktop.ini')
    >>> pidl = shell.SHParseDisplayName(path, 0, None)[0]
    >>> flags = (shellcon.SHGFI_PIDL |
    ...          shellcon.SHGFI_SYSICONINDEX |
    ...          shellcon.SHGFI_ICON |
    ...          shellcon.SHGFI_DISPLAYNAME)
    >>> shell.SHGetFileInfo(pidl, 0, flags)
    
    Breakpoint 0 hit
    SHELL32!SHGetFileInfoW:
    000007fe`fd692290 fff3            push    rbx
    0:000> k 5
    *** WARNING: Unable to verify checksum for
    C:\Temp\test\lib\site-packages\win32comext\shell\shell.pyd
    Child-SP          RetAddr           Call Site
    00000000`003ff2d8 00000000`5f44c5e8 SHELL32!SHGetFileInfoW
    00000000`003ff2e0 00000000`5f5af8bd shell!PySHGetFileInfo+0xf8
    00000000`003ff610 00000000`5f62385b python34!PyCFunction_Call+0x12d
    00000000`003ff640 00000000`5f625c89 python34!call_function+0x2ab
    00000000`003ff6a0 00000000`5f62770c python34!PyEval_EvalFrameEx+0x2279
    0:000> r rcx
    rcx=0000000000000000
    0:000> g
    (0, (0, 0, 0, '', ''))
    

    In the Windows x64 ABI, the first argument of a function is passed in register rcx. We know from the SHGetFileInfo docs that this should be the PIDL, but actually NULL is being passed. Clearly this is a bug. The stack trace lays the blame on shell!PySHGetFileInfo. Here's a snippet of the problematic code:

        if (flags & SHGFI_PIDL) {
            ok = PyObject_AsPIDL(obName, &pidl, FALSE);
            pidl_or_name = (TCHAR *)pidl;
        } else {
            ok = PyWinObject_AsTCHAR(obName, &name, FALSE);
            pidl_or_name = name;
        }
        if (!ok)
            return NULL;
        SHFILEINFO info;
        memset(&info, 0, sizeof(info));
        info.dwAttributes = info_attrs;
        PY_INTERFACE_PRECALL;
        DWORD_PTR dw = SHGetFileInfo(name, attr, &info, sizeof(info), flags);
    

    The mistake is passing name as the first argument instead of pidl_or_name.


    The question is tagged ctypes. IMO, using ctypes is worth it if doing so eliminates a large dependency such as PyWin32. I wouldn't normally use ctypes by itself for a COM-based API. The comtypes package builds on ctypes if you want to try that. In this case directly calling COM methods can be avoided by instead calling SHParseDisplayName. Other than using HRESULT return codes, it's pretty much like any other Win32 API.

    import types as _types
    import ctypes as _ctypes
    from ctypes import wintypes as _wtypes
    _mtypes = _types.ModuleType('_mtypes')
    
    _ole32 = _ctypes.WinDLL('ole32')
    _shell32 = _ctypes.WinDLL('shell32')
    _user32 = _ctypes.WinDLL('user32')
    
    try:
        from win32com.shell import shell as _shell
    except ImportError:
        _shell = None
    
    try:
        from win32com.shell import shellcon
    except ImportError:
        shellcon = _types.ModuleType('shellcon')
        shellcon.SHGFI_LARGEICON         = 0x00000
        shellcon.SHGFI_SMALLICON         = 0x00001
        shellcon.SHGFI_OPENICON          = 0x00002
        shellcon.SHGFI_SHELLICONSIZE     = 0x00004
        shellcon.SHGFI_PIDL              = 0x00008
        shellcon.SHGFI_USEFILEATTRIBUTES = 0x00010
        shellcon.SHGFI_ICON              = 0x00100
        shellcon.SHGFI_DISPLAYNAME       = 0x00200
        shellcon.SHGFI_TYPENAME          = 0x00400
        shellcon.SHGFI_ATTRIBUTES        = 0x00800
        shellcon.SHGFI_ICONLOCATION      = 0x01000
        shellcon.SHGFI_EXETYPE           = 0x02000
        shellcon.SHGFI_SYSICONINDEX      = 0x04000
        shellcon.SHGFI_LINKOVERLAY       = 0x08000
        shellcon.SHGFI_SELECTED          = 0x10000
        shellcon.SHGFI_ATTR_SPECIFIED    = 0x20000
    
    try:
        import win32con
    except ImportError:
        win32con = _types.ModuleType('win32con')
        win32con.MAX_PATH = 260
        win32con.FILE_ATTRIBUTE_READONLY            = 0x00001
        win32con.FILE_ATTRIBUTE_HIDDEN              = 0x00002
        win32con.FILE_ATTRIBUTE_SYSTEM              = 0x00004
        win32con.FILE_ATTRIBUTE_DIRECTORY           = 0x00010
        win32con.FILE_ATTRIBUTE_ARCHIVE             = 0x00020
        win32con.FILE_ATTRIBUTE_DEVICE              = 0x00040
        win32con.FILE_ATTRIBUTE_NORMAL              = 0x00080
        win32con.FILE_ATTRIBUTE_TEMPORARY           = 0x00100
        win32con.FILE_ATTRIBUTE_ATOMIC_WRITE        = 0x00200
        win32con.FILE_ATTRIBUTE_SPARSE_FILE         = 0x00200
        win32con.FILE_ATTRIBUTE_REPARSE_POINT       = 0x00400
        win32con.FILE_ATTRIBUTE_XACTION_WRITE       = 0x00400
        win32con.FILE_ATTRIBUTE_COMPRESSED          = 0x00800
        win32con.FILE_ATTRIBUTE_OFFLINE             = 0x01000
        win32con.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x02000
        win32con.FILE_ATTRIBUTE_ENCRYPTED           = 0x04000
        win32con.FILE_ATTRIBUTE_VIRTUAL             = 0x10000
    
    _mtypes.CData = _ctypes.Array.__bases__[0]
    _mtypes.PPIDLIST_ABSOLUTE = _ctypes.POINTER(_ctypes.c_void_p)
    _mtypes.SFGAOF = _wtypes.ULONG
    _mtypes.PSFGAOF = _ctypes.POINTER(_mtypes.SFGAOF)
    
    _ole32.CoInitialize.restype = _ctypes.HRESULT # checked
    _ole32.CoInitialize.argtypes = (_ctypes.c_void_p,)
    _ole32.CoUninitialize.restype = None
    _ole32.CoUninitialize.argtypes = ()
    _ole32.CoTaskMemFree.restype = None
    _ole32.CoTaskMemFree.argtypes = (_ctypes.c_void_p,)
    _user32.DestroyIcon.argtypes = (_wtypes.HICON,)
    
    _shell32.SHParseDisplayName.restype = _ctypes.HRESULT # checked
    _shell32.SHParseDisplayName.argtypes = (
        _wtypes.LPCWSTR,           # pszName,   _In_
        _ctypes.c_void_p,          # pbc,       _In_opt_
        _mtypes.PPIDLIST_ABSOLUTE, # ppidl,     _Out_
        _mtypes.SFGAOF,            # sfgaoIn,   _In_
        _mtypes.PSFGAOF)           # psfgaoOut, _Out_opt_
    
    class SHFILEINFO(_ctypes.Structure):
       _fields_ = (('hIcon', _wtypes.HICON),
                   ('iIcon', _ctypes.c_int),
                   ('dwAttributes', _wtypes.DWORD),
                   ('szDisplayName', _wtypes.WCHAR * win32con.MAX_PATH),
                   ('szTypeName', _wtypes.WCHAR * 80))
    
    _mtypes.SHFILEINFO = SHFILEINFO
    _mtypes.PSHFILEINFO = _ctypes.POINTER(SHFILEINFO)
    
    _shell32.SHGetFileInfoW.restype = _ctypes.c_void_p
    _shell32.SHGetFileInfoW.argtypes = (
        _wtypes.LPVOID,      # pszPath,          _In_
        _wtypes.DWORD,       # dwFileAttributes,
        _mtypes.PSHFILEINFO, # psfi,             _Inout_
        _wtypes.UINT,        # cbFileInfo,
        _wtypes.UINT)        # uFlags
    
    def SHGetFileInfo(pidl, attributes=0, flags=0):
        if _shell is not None:
            if not isinstance(pidl, (str, bytes, _mtypes.CData)):
                pidl = _shell.PIDLAsString(pidl)
        finfo = SHFILEINFO()
        _ole32.CoInitialize(None)    
        try:
            retval = _shell32.SHGetFileInfoW(pidl,
                                             attributes,
                                             _ctypes.byref(finfo),
                                             _ctypes.sizeof(finfo),
                                             flags)
        finally:
            _ole32.CoUninitialize()
        if not retval:            
            if flags != shellcon.SHGFI_EXETYPE:
                raise _ctypes.WinError()
        return retval, finfo
    

    Example:

    if __name__ == '__main__':
        import os    
        path = os.path.expanduser(r'~\Desktop\desktop.ini')
        pidl = _shell.SHParseDisplayName(path, 0)[0]
        assert isinstance(pidl, list)
    
        flags = (shellcon.SHGFI_PIDL |
                 shellcon.SHGFI_ICON |
                 shellcon.SHGFI_DISPLAYNAME |
                 shellcon.SHGFI_TYPENAME |
                 shellcon.SHGFI_ATTRIBUTES |
                 shellcon.SHGFI_SYSICONINDEX)
    
        hImageList, finfo = SHGetFileInfo(pidl, 0, flags)
    
        print('hImageList:', hImageList)
        for name, typ in finfo._fields_:
            print(name, ': ', ascii(getattr(finfo, name)), sep='')
    
        if finfo.hIcon:
            _user32.DestroyIcon(finfo.hIcon)
    

    Output:

    hImageList: 4411024
    hIcon: 10617107
    iIcon: 7
    dwAttributes: 1078497655
    szDisplayName: 'desktop.ini'
    szTypeName: 'Configuration settings'