How to use SHGetFileInfo with SHGFI_PIDL in python

2020-07-30 10:55发布

问题:

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!

回答1:

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:

  • 64-bit Windows 7 SP1 (6.1.7601)
  • Windows SDK 7.1 (ensure the debuggers are installed)
  • Visual Studio 2010 SP1
  • Visual C++ 2010 SP1 Compiler Update
  • Python 3.4 (and debug files)
  • Mercurial (hg.exe, not TortoiseHg)

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'