1 /*
2  * PROJECT:     shlextdbg
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     Shell extension debug utility
5  * COPYRIGHT:   Copyright 2017 Mark Jansen (mark.jansen@reactos.org)
6  */
7 
8 #include <windows.h>
9 #include <shlobj.h>
10 #include <shlwapi.h>
11 #include <atlbase.h>   // thanks gcc
12 #include <atlcom.h>    // thanks gcc
13 #include <atlstr.h>
14 #include <atlsimpcoll.h>
15 #include <conio.h>
16 #include <shellutils.h>
17 #include <shlwapi_undoc.h>
18 
19 static void PrintHelp(PCWSTR ExtraLine = NULL)
20 {
21     if (ExtraLine)
22         wprintf(L"%s\n\n", ExtraLine);
23 
24     wprintf(L"shlextdbg /clsid={clsid} [/dll=dllname] /IShellExtInit=filename |shlextype| |waitoptions|\n");
25     wprintf(L"    {clsid}: The CLSID or ProgID of the object to create\n");
26     wprintf(L"    dll: Optional dllname to create the object from, instead of CoCreateInstance\n");
27     wprintf(L"    filename: The filename to pass to IShellExtInit->Initialze\n");
28     wprintf(L"    shlextype: The type of shell extention to run:\n");
29     wprintf(L"               /IShellPropSheetExt to create a property sheet\n");
30     wprintf(L"               /IContextMenu=verb to activate the specified verb\n");
31     wprintf(L"    waitoptions: Specify how to wait:\n");
32     wprintf(L"                 /explorerinstance: Wait for SHGetInstanceExplorer (Default)\n");
33     wprintf(L"                 /infinite: Keep on waiting infinitely\n");
34     wprintf(L"                 /openwindows: Wait for all windows from the current application to close\n");
35     wprintf(L"                 /input: Wait for input\n");
36     wprintf(L"                 /nowait\n");
37     wprintf(L"\n");
38     wprintf(L"shlextdbg /shgfi=path\n");
39     wprintf(L"    Call SHGetFileInfo. Prefix path with $ to parse as a pidl.\n");
40     wprintf(L"\n");
41     wprintf(L"shlextdbg /assocq <[{bhid}]path> <string|data|key> <type> <initflags> <queryflags> <initstring> [extra] [maxsize]\n");
42     wprintf(L"    Uses the default implementation from AssocCreate if path is empty.\n");
43     wprintf(L"\n");
44     wprintf(L"shlextdbg /shellexec=path [/see] [verb] [class]\n");
45     wprintf(L"\n");
46     wprintf(L"shlextdbg /dumpmenu=[{clsid}]path [/cmf]\n");
47 }
48 
49 /*
50 Examples:
51 
52 /clsid={513D916F-2A8E-4F51-AEAB-0CBC76FB1AF8} /IShellExtInit=C:\RosBE\Uninstall.exe /IShellPropSheetExt
53 /clsid=CompressedFolder /IShellExtInit=e:\test.zip /IContextMenu=extract /openwindows
54 /clsid=CompressedFolder /IShellExtInit=e:\test.zip /IContextMenu=extract /openwindows /dll=R:\build\dev\devenv\dll\shellext\zipfldr\Debug\zipfldr.dll
55 /shgfi=c:\freeldr.ini
56 /assocq "" string 1 0 0 .txt
57 /assocq "" string friendlytypename 0x400 0 .txt "" 10
58 /openwindows /shellexec=c: /invoke properties
59 /dumpmenu=%windir%\explorer.exe /extended
60 /dumpmenu {D969A300-E7FF-11d0-A93B-00A0C90F2719}c:
61 
62 */
63 
64 static LONG StrToNum(PCWSTR in)
65 {
66     PWCHAR end;
67     LONG v = wcstol(in, &end, 0);
68     return (end > in) ? v : 0;
69 }
70 
71 static int ErrMsg(int Error)
72 {
73     WCHAR buf[400];
74     for (UINT e = Error, cch; ;)
75     {
76         lstrcpynW(buf, L"?", _countof(buf));
77         cch = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, 0, e, 0, buf, _countof(buf), NULL);
78         while (cch && buf[cch - 1] <= ' ')
79             buf[--cch] = UNICODE_NULL; // Remove trailing newlines
80         if (cch || HIWORD(e) != HIWORD(HRESULT_FROM_WIN32(1)))
81             break;
82         e = HRESULT_CODE(e); // "WIN32_FROM_HRESULT"
83     }
84     wprintf(Error < 0 ? L"Error 0x%.8X %s\n" : L"Error %d %s\n", Error, buf);
85     return Error;
86 }
87 
88 template<class T>
89 static bool CLSIDPrefix(T& String, CLSID& Clsid)
90 {
91     WCHAR buf[38 + 1];
92     if (String[0] == '{')
93     {
94         lstrcpynW(buf, String, _countof(buf));
95         if (SUCCEEDED(CLSIDFromString(buf, &Clsid)))
96         {
97             String = String + 38;
98             return true;
99         }
100     }
101     return false;
102 }
103 
104 static HRESULT GetUIObjectOfAbsolute(LPCITEMIDLIST pidl, REFIID riid, void** ppv)
105 {
106     CComPtr<IShellFolder> shellFolder;
107     PCUITEMID_CHILD child;
108     HRESULT hr = SHBindToParent(pidl, IID_PPV_ARG(IShellFolder, &shellFolder), &child);
109     if (SUCCEEDED(hr))
110         hr = shellFolder->GetUIObjectOf(NULL, 1, &child, riid, NULL, ppv);
111     return hr;
112 }
113 
114 static HRESULT CreateShellItemFromParse(PCWSTR Path, IShellItem** ppSI)
115 {
116     PIDLIST_ABSOLUTE pidl = NULL;
117     HRESULT hr = SHParseDisplayName(Path, NULL, &pidl, 0, NULL);
118     if (SUCCEEDED(hr))
119     {
120         hr = SHCreateShellItem(NULL, NULL, pidl, ppSI);
121         SHFree(pidl);
122     }
123     return hr;
124 }
125 
126 static void GetAssocClass(LPCWSTR Path, LPCITEMIDLIST pidl, HKEY& hKey)
127 {
128     hKey = NULL;
129     IQueryAssociations* pQA;
130     if (SUCCEEDED(GetUIObjectOfAbsolute(pidl, IID_PPV_ARG(IQueryAssociations, &pQA))))
131     {
132         pQA->GetKey(0, ASSOCKEY_CLASS, NULL, &hKey); // Not implemented in ROS
133         pQA->Release();
134     }
135     if (!hKey)
136     {
137         DWORD cb;
138         WCHAR buf[MAX_PATH];
139         PWSTR ext = PathFindExtensionW(Path);
140         SHFILEINFOW info;
141         info.dwAttributes = 0;
142         SHGetFileInfoW((LPWSTR)pidl, 0, &info, sizeof(info), SHGFI_PIDL | SHGFI_ATTRIBUTES);
143         if (info.dwAttributes & SFGAO_FOLDER)
144         {
145             ext = const_cast<LPWSTR>(L"Directory");
146         }
147         else if (info.dwAttributes & SFGAO_BROWSABLE)
148         {
149             ext = const_cast<LPWSTR>(L"Folder"); // Best guess
150         }
151         else
152         {
153             cb = sizeof(buf);
154             if (!SHGetValueW(HKEY_CLASSES_ROOT, ext, NULL, NULL, buf, &cb))
155             {
156                 RegOpenKeyExW(HKEY_CLASSES_ROOT, buf, 0, KEY_READ, &hKey);
157             }
158         }
159         if (!hKey)
160         {
161             RegOpenKeyExW(HKEY_CLASSES_ROOT, ext, 0, KEY_READ, &hKey);
162         }
163     }
164 }
165 
166 static void DumpBytes(const void *Data, SIZE_T cb)
167 {
168     for (SIZE_T i = 0; i < cb; ++i)
169     {
170         wprintf(L"%s%.2X", i ? L" " : L"", ((LPCBYTE)Data)[i]);
171     }
172     wprintf(L"\n");
173 }
174 
175 static HRESULT GetCommandString(IContextMenu& CM, UINT Id, UINT Type, LPWSTR buf, UINT cchMax)
176 {
177     if (cchMax < 1) return E_INVALIDARG;
178     *buf = UNICODE_NULL;
179 
180     // First try to retrieve the UNICODE string directly
181     HRESULT hr = CM.GetCommandString(Id, Type | GCS_UNICODE, 0, (char*)buf, cchMax);
182     if (FAILED(hr))
183     {
184         // It failed, try to retrieve an ANSI string instead then convert it to UNICODE
185         STRRET sr;
186         sr.uType = STRRET_CSTR;
187         hr = CM.GetCommandString(Id, Type & ~GCS_UNICODE, 0, sr.cStr, _countof(sr.cStr));
188         if (SUCCEEDED(hr))
189             hr = StrRetToBufW(&sr, NULL, buf, cchMax);
190     }
191     return hr;
192 }
193 
194 static void DumpMenu(HMENU hMenu, UINT IdOffset, IContextMenu* pCM, BOOL FakeInit, UINT Indent)
195 {
196     bool recurse = Indent != UINT(-1);
197     WCHAR buf[MAX_PATH];
198     MENUITEMINFOW mii;
199     mii.cbSize = FIELD_OFFSET(MENUITEMINFOW, hbmpItem);
200 
201     for (UINT i = 0, defid = GetMenuDefaultItem(hMenu, FALSE, 0); ; ++i)
202     {
203         mii.fMask = MIIM_STRING;
204         mii.dwTypeData = buf;
205         mii.cch = _countof(buf);
206         *buf = UNICODE_NULL;
207         if (!GetMenuItemInfo(hMenu, i, TRUE, &mii))
208             lstrcpynW(buf, L"?", _countof(buf)); // Tolerate string failure
209         mii.fMask = MIIM_ID | MIIM_SUBMENU | MIIM_FTYPE;
210         mii.hSubMenu = NULL;
211         mii.dwTypeData = NULL;
212         mii.cch = 0;
213         if (!GetMenuItemInfo(hMenu, i, TRUE, &mii))
214             break;
215 
216         BOOL sep = mii.fType & MFT_SEPARATOR;
217         wprintf(L"%-4d", (sep || mii.wID == UINT(-1)) ? mii.wID : (mii.wID - IdOffset));
218         for (UINT j = 0; j < Indent && recurse; ++j)
219             wprintf(L" ");
220         wprintf(L"%s%s", mii.hSubMenu ? L">" : L"|", sep ? L"----------" : buf);
221         if (!sep && pCM && SUCCEEDED(GetCommandString(*pCM, mii.wID - IdOffset,
222                                                       GCS_VERB, buf, _countof(buf))))
223         {
224             wprintf(L" [%s]", buf);
225         }
226         wprintf(L"%s\n", (defid == mii.wID && defid != UINT(-1)) ? L" (Default)" : L"");
227         if (mii.hSubMenu && recurse)
228         {
229             if (FakeInit)
230                 SHForwardContextMenuMsg(pCM, WM_INITMENUPOPUP, (WPARAM)mii.hSubMenu, LOWORD(i), NULL, TRUE);
231             DumpMenu(mii.hSubMenu, IdOffset, pCM, FakeInit, Indent + 1);
232         }
233     }
234 }
235 
236 static int SHGFI(PCWSTR Path)
237 {
238     PIDLIST_ABSOLUTE pidl = NULL;
239     UINT flags = 0, ret = 0;
240 
241     if (*Path == L'$')
242     {
243         HRESULT hr = SHParseDisplayName(++Path, NULL, &pidl, 0, NULL);
244         if (FAILED(hr))
245             return ErrMsg(hr);
246         flags |= SHGFI_PIDL;
247         Path = (LPCWSTR)pidl;
248     }
249     else if (GetFileAttributes(Path) == INVALID_FILE_ATTRIBUTES)
250     {
251         flags |= SHGFI_USEFILEATTRIBUTES;
252     }
253     SHFILEINFOW info;
254     if (!SHGetFileInfoW(Path, 0, &info, sizeof(info), flags |
255                         SHGFI_DISPLAYNAME | SHGFI_ATTRIBUTES | SHGFI_TYPENAME))
256     {
257         info.szDisplayName[0] = info.szTypeName[0] = UNICODE_NULL;
258         info.dwAttributes = 0;
259         ret = ERROR_FILE_NOT_FOUND;
260     }
261     wprintf(L"Display: %s\n", info.szDisplayName);
262     wprintf(L"Attributes: 0x%x\n", info.dwAttributes);
263     wprintf(L"Type: %s\n", info.szTypeName);
264 
265     if (!SHGetFileInfoW(Path, 0, &info, sizeof(info), flags | SHGFI_ICONLOCATION))
266     {
267         info.szDisplayName[0] = UNICODE_NULL;
268         info.iIcon = -1;
269     }
270     wprintf(L"Icon: %s,%d\n", info.szDisplayName, info.iIcon);
271 
272     if (!SHGetFileInfoW(Path, 0, &info, sizeof(info), flags | SHGFI_SYSICONINDEX))
273     {
274         info.iIcon = -1;
275     }
276     wprintf(L"Index: %d\n", info.iIcon);
277     SHFree(pidl);
278     return ret;
279 }
280 
281 static HRESULT AssocQ(int argc, WCHAR **argv)
282 {
283     UINT qtype = StrToNum(argv[2]);
284     ASSOCF iflags = StrToNum(argv[3]);
285     ASSOCF qflags = StrToNum(argv[4]);
286     PCWSTR extra = (argc > 6 && *argv[6]) ? argv[6] : NULL;
287     WCHAR buf[MAX_PATH * 2];
288     DWORD maxSize = (argc > 7 && *argv[7]) ? StrToNum(argv[7]) : sizeof(buf);
289 
290     HRESULT hr;
291     CComPtr<IQueryAssociations> qa;
292     PWSTR path = argv[0];
293     if (*path)
294     {
295         CLSID clsid, *pclsid = NULL;
296         if (CLSIDPrefix(path, clsid))
297             pclsid = &clsid;
298         CComPtr<IShellItem> si;
299         if (SUCCEEDED(hr = CreateShellItemFromParse(path, &si)))
300         {
301             hr = si->BindToHandler(NULL, pclsid ? *pclsid : BHID_AssociationArray, IID_PPV_ARG(IQueryAssociations, &qa));
302             if (FAILED(hr) && !pclsid)
303                 hr = si->BindToHandler(NULL, BHID_SFUIObject, IID_PPV_ARG(IQueryAssociations, &qa));
304         }
305     }
306     else
307     {
308         hr = AssocCreate(CLSID_QueryAssociations, IID_PPV_ARG(IQueryAssociations, &qa));
309     }
310     if (FAILED(hr))
311         return ErrMsg(hr);
312     hr = qa->Init(iflags, argv[5], NULL, NULL);
313     if (FAILED(hr))
314         return ErrMsg(hr);
315 
316     DWORD size = maxSize;
317     if (!wcsicmp(argv[1], L"string"))
318     {
319         if (!wcsicmp(argv[2], L"COMMAND"))
320             qtype = ASSOCSTR_COMMAND;
321         if (!wcsicmp(argv[2], L"EXECUTABLE"))
322             qtype = ASSOCSTR_EXECUTABLE;
323         if (!wcsicmp(argv[2], L"FRIENDLYDOCNAME") || !wcsicmp(argv[2], L"FriendlyTypeName"))
324             qtype = ASSOCSTR_FRIENDLYDOCNAME;
325         if (!wcsicmp(argv[2], L"DEFAULTICON"))
326             qtype = ASSOCSTR_DEFAULTICON;
327 
328         buf[0] = UNICODE_NULL;
329         size /= sizeof(buf[0]); // Convert to number of characters
330         hr = qa->GetString(qflags, (ASSOCSTR)qtype, extra, buf, &size);
331         size *= sizeof(buf[0]); // Convert back to bytes
332         if (SUCCEEDED(hr) ||
333             hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER))
334         {
335             wprintf(L"0x%.8X: %s\n", hr, buf);
336         }
337         else
338         {
339             wprintf(size != maxSize ? L"%u " : L"", size);
340             ErrMsg(hr);
341         }
342     }
343     else if (!wcsicmp(argv[1], L"data"))
344     {
345         if (!wcsicmp(argv[2], L"EDITFLAGS"))
346             qtype = ASSOCDATA_EDITFLAGS;
347         if (!wcsicmp(argv[2], L"VALUE"))
348             qtype = ASSOCDATA_VALUE;
349 
350         hr = qa->GetData(qflags, (ASSOCDATA)qtype, extra, buf, &size);
351         if (SUCCEEDED(hr))
352         {
353             wprintf(L"0x%.8X: %u byte(s) ", hr, size);
354             DumpBytes(buf, min(size, maxSize));
355         }
356         else
357         {
358             wprintf(size != maxSize ? L"%u " : L"", size);
359             ErrMsg(hr);
360         }
361     }
362     else if (!wcsicmp(argv[1], L"key"))
363     {
364         HKEY hKey = NULL;
365         hr = qa->GetKey(qflags, (ASSOCKEY)qtype, extra, &hKey);
366         if (SUCCEEDED(hr))
367         {
368             wprintf(L"0x%.8X: hKey %p\n", hr, hKey);
369             RegQueryValueExW(hKey, L"shlextdbg", 0, NULL, NULL, NULL); // Filter by this in Process Monitor
370             RegCloseKey(hKey);
371         }
372         else
373         {
374             ErrMsg(hr);
375         }
376     }
377     else
378     {
379         PrintHelp(L"Unknown query");
380         return ErrMsg(ERROR_INVALID_PARAMETER);
381     }
382     return hr;
383 }
384 
385 enum WaitType
386 {
387     Wait_None,
388     Wait_Infinite,
389     Wait_OpenWindows,
390     Wait_Input,
391     Wait_ExplorerInstance,
392 };
393 
394 CLSID g_CLSID = { 0 };
395 CStringW g_DLL;
396 CStringW g_ShellExtInit;
397 bool g_bIShellPropSheetExt = false;
398 CStringA g_ContextMenu;
399 WaitType g_Wait = Wait_ExplorerInstance;
400 
401 HRESULT CreateIDataObject(CComHeapPtr<ITEMIDLIST>& pidl, CComPtr<IDataObject>& dataObject, PCWSTR FileName)
402 {
403     HRESULT hr = SHParseDisplayName(FileName, NULL, &pidl, 0, NULL);
404     if (!SUCCEEDED(hr))
405     {
406         wprintf(L"Failed to create pidl from '%s': 0x%x\n", FileName, hr);
407         return hr;
408     }
409 
410     CComPtr<IShellFolder> shellFolder;
411     PCUITEMID_CHILD childs;
412     hr = SHBindToParent(pidl, IID_PPV_ARG(IShellFolder, &shellFolder), &childs);
413     if (!SUCCEEDED(hr))
414     {
415         wprintf(L"Failed to bind to parent: 0x%x\n", hr);
416         return hr;
417     }
418     hr = shellFolder->GetUIObjectOf(NULL, 1, &childs, IID_IDataObject, NULL, (PVOID*)&dataObject);
419     if (!SUCCEEDED(hr))
420     {
421         wprintf(L"Failed to query IDataObject: 0x%x\n", hr);
422     }
423     return hr;
424 }
425 
426 HRESULT LoadAndInitialize(REFIID riid, LPVOID* ppv)
427 {
428     CComPtr<IShellExtInit> spShellExtInit;
429     HRESULT hr;
430     if (g_DLL.IsEmpty())
431     {
432         hr = CoCreateInstance(g_CLSID, NULL, CLSCTX_ALL, IID_PPV_ARG(IShellExtInit, &spShellExtInit));
433         if (!SUCCEEDED(hr))
434         {
435             WCHAR Buffer[100];
436             StringFromGUID2(g_CLSID, Buffer, _countof(Buffer));
437             wprintf(L"Failed to Create %s:IShellExtInit: 0x%x\n", Buffer, hr);
438             return hr;
439         }
440     }
441     else
442     {
443         typedef HRESULT (STDAPICALLTYPE *tDllGetClassObject)(REFCLSID rclsid, REFIID riid, LPVOID *ppv);
444         HMODULE mod = LoadLibraryW(g_DLL);
445         if (!mod)
446         {
447             wprintf(L"Failed to Load %s:(0x%x)\n", g_DLL.GetString(), GetLastError());
448             return E_FAIL;
449         }
450         tDllGetClassObject DllGet = (tDllGetClassObject)GetProcAddress(mod, "DllGetClassObject");
451         if (!DllGet)
452         {
453             wprintf(L"%s does not export DllGetClassObject\n", g_DLL.GetString());
454             return E_FAIL;
455         }
456         CComPtr<IClassFactory> spClassFactory;
457         hr = DllGet(g_CLSID, IID_PPV_ARG(IClassFactory, &spClassFactory));
458         if (!SUCCEEDED(hr))
459         {
460             wprintf(L"Failed to create IClassFactory: 0x%x\n", hr);
461             return hr;
462         }
463         hr = spClassFactory->CreateInstance(NULL, IID_PPV_ARG(IShellExtInit, &spShellExtInit));
464         if (!SUCCEEDED(hr))
465         {
466             wprintf(L"Failed to Request IShellExtInit from IClassFactory: 0x%x\n", hr);
467             return hr;
468         }
469     }
470 
471     CComPtr<IDataObject> spDataObject;
472     CComHeapPtr<ITEMIDLIST> pidl;
473     hr = CreateIDataObject(pidl, spDataObject, g_ShellExtInit.GetString());
474     if (!SUCCEEDED(hr))
475         return hr;
476 
477     HKEY hKey = NULL;
478     GetAssocClass(g_ShellExtInit.GetString(), pidl, hKey);
479     hr = spShellExtInit->Initialize(pidl, spDataObject, hKey);
480     if (hKey)
481         RegCloseKey(hKey);
482     if (!SUCCEEDED(hr))
483     {
484         wprintf(L"IShellExtInit->Initialize failed: 0x%x\n", hr);
485         return hr;
486     }
487     hr = spShellExtInit->QueryInterface(riid, ppv);
488     if (!SUCCEEDED(hr))
489     {
490         WCHAR Buffer[100];
491         StringFromGUID2(riid, Buffer, _countof(Buffer));
492         wprintf(L"Failed to query %s from IShellExtInit: 0x%x\n", Buffer, hr);
493     }
494     return hr;
495 }
496 
497 
498 CSimpleArray<HWND> g_Windows;
499 HWND g_ConsoleWindow;
500 BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
501 {
502     if (hwnd != g_ConsoleWindow)
503     {
504         DWORD pid = 0;
505         GetWindowThreadProcessId(hwnd, &pid);
506         if (pid == GetCurrentProcessId())
507         {
508             g_Windows.Add(hwnd);
509         }
510     }
511     return TRUE;
512 }
513 
514 void WaitWindows()
515 {
516     /* Give the windows some time to spawn */
517     Sleep(2000);
518     g_ConsoleWindow = GetConsoleWindow();
519     while (true)
520     {
521         g_Windows.RemoveAll();
522         EnumWindows(EnumWindowsProc, NULL);
523         if (g_Windows.GetSize() == 0)
524             break;
525         Sleep(500);
526     }
527     wprintf(L"All windows closed (ignoring console window)\n");
528 }
529 
530 struct ExplorerInstance : public IUnknown
531 {
532     HWND m_hWnd;
533     volatile LONG m_rc;
534 
535     ExplorerInstance() : m_hWnd(NULL), m_rc(1) {}
536     virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv)
537     {
538         static const QITAB rgqit[] = { { 0 } };
539         return QISearch(this, rgqit, riid, ppv);
540     }
541     virtual ULONG STDMETHODCALLTYPE AddRef()
542     {
543         if (g_Wait == Wait_ExplorerInstance)
544             wprintf(L"INFO: SHGetInstanceExplorer\n");
545         return InterlockedIncrement(&m_rc);
546     }
547     virtual ULONG STDMETHODCALLTYPE Release()
548     {
549         if (g_Wait == Wait_ExplorerInstance)
550             wprintf(L"INFO: Release ExplorerInstance\n");
551         ULONG r = InterlockedDecrement(&m_rc);
552         if (!r)
553             PostMessage(m_hWnd, WM_CLOSE, 0, 0);
554         return r;
555     }
556     void Wait()
557     {
558         SHSetInstanceExplorer(NULL);
559         m_hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, TEXT("STATIC"), NULL, WS_POPUP,
560                                 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL);
561         BOOL loop = InterlockedDecrement(&m_rc) != 0;
562         MSG msg;
563         while (loop && (int)GetMessage(&msg, NULL, 0, 0) > 0)
564         {
565             if (msg.hwnd == m_hWnd && msg.message == WM_CLOSE)
566                 PostMessage(m_hWnd, WM_QUIT, 0, 0);
567             DispatchMessage(&msg);
568         }
569     }
570 } g_EI;
571 
572 static void Wait()
573 {
574     LPCWSTR nag = L"(Please use SHGetInstanceExplorer in your code instead)";
575     switch (g_Wait)
576     {
577     case Wait_None:
578         break;
579     case Wait_Infinite:
580         _putws(nag);
581         while (true)
582             Sleep(1000);
583         break;
584     case Wait_OpenWindows:
585         _putws(nag);
586         WaitWindows();
587         break;
588     case Wait_Input:
589         wprintf(L"Press any key to continue... %s\n", nag);
590         _getch();
591         break;
592     case Wait_ExplorerInstance:
593         g_EI.Wait();
594         break;
595     }
596 }
597 
598 CSimpleArray<HPROPSHEETPAGE> g_Pages;
599 static BOOL CALLBACK cb_AddPage(HPROPSHEETPAGE page, LPARAM lParam)
600 {
601     g_Pages.Add(page);
602     if (lParam != (LPARAM)&g_Pages)
603     {
604         wprintf(L"Propsheet failed to pass lParam, got: 0x%Ix\n", lParam);
605     }
606     return TRUE;
607 }
608 
609 static bool isCmdWithArg(int argc, WCHAR** argv, int& n, PCWSTR check, PCWSTR &arg)
610 {
611     arg = NULL;
612     size_t len = wcslen(check);
613     if (!_wcsnicmp(argv[n] + 1, check, len))
614     {
615         PCWSTR cmd = argv[n] + len + 1;
616         if (*cmd == ':' || *cmd == '=')
617         {
618             arg = cmd + 1;
619             return true;
620         }
621         if (n + 1 < argc)
622         {
623             arg = argv[n+1];
624             n++;
625             return true;
626         }
627         wprintf(L"Command %s has no required argument!\n", check);
628         return false;
629     }
630     return false;
631 }
632 
633 static bool isCmd(int argc, WCHAR** argv, int n, PCWSTR check)
634 {
635     return !wcsicmp(argv[n] + 1, check);
636 }
637 
638 extern "C"  // and another hack for gcc
639 int wmain(int argc, WCHAR **argv)
640 {
641     INITCOMMONCONTROLSEX icc = { sizeof(icc), ICC_LINK_CLASS | ICC_STANDARD_CLASSES };
642     InitCommonControlsEx(&icc);
643     CoInitialize(NULL);
644     SHSetInstanceExplorer(static_cast<IUnknown*>(&g_EI));
645 
646     bool failArgs = false;
647     for (int n = 1; n < argc; ++n)
648     {
649         WCHAR* cmd = argv[n];
650         if (cmd[0] == '-' || cmd[0] == '/')
651         {
652             PCWSTR arg;
653             if (isCmdWithArg(argc, argv, n, L"shgfi", arg))
654             {
655                 failArgs = true;
656                 if (*arg)
657                     return SHGFI(arg);
658             }
659             else if (isCmd(argc, argv, n, L"assocq"))
660             {
661                 failArgs = true;
662                 if (argc - (n + 1) >= 6 && argc - (n + 1) <= 8)
663                     return AssocQ(argc - (n + 1), &argv[(n + 1)]);
664             }
665             else if (isCmdWithArg(argc, argv, n, L"shellexec", arg))
666             {
667                 PIDLIST_ABSOLUTE pidl = NULL;
668                 HRESULT hr = SHParseDisplayName(arg, NULL, &pidl, 0, NULL);
669                 if (FAILED(hr))
670                     return ErrMsg(hr);
671                 SHELLEXECUTEINFOW sei = { sizeof(sei), SEE_MASK_IDLIST | SEE_MASK_UNICODE };
672                 sei.lpIDList = pidl;
673                 sei.nShow = SW_SHOW;
674                 while (++n < argc)
675                 {
676                     if (argv[n][0] != '-' && argv[n][0] != '/')
677                         break;
678                     else if (isCmd(argc, argv, n, L"INVOKE"))
679                         sei.fMask |= SEE_MASK_INVOKEIDLIST;
680                     else if (isCmd(argc, argv, n, L"NOUI"))
681                         sei.fMask |= SEE_MASK_FLAG_NO_UI;
682                     else if (isCmd(argc, argv, n, L"ASYNCOK"))
683                         sei.fMask |= SEE_MASK_ASYNCOK ;
684                     else if (isCmd(argc, argv, n, L"NOASYNC"))
685                         sei.fMask |= SEE_MASK_NOASYNC;
686                     else
687                         wprintf(L"WARN: Ignoring switch %s\n", argv[n]);
688                 }
689                 if (n < argc && *argv[n++])
690                 {
691                     sei.lpVerb = argv[n - 1];
692                 }
693                 if (n < argc && *argv[n++])
694                 {
695                     sei.lpClass = argv[n - 1];
696                     sei.fMask |= SEE_MASK_CLASSNAME;
697                 }
698                 UINT succ = ShellExecuteExW(&sei), gle = GetLastError();
699                 SHFree(pidl);
700                 if (!succ)
701                     return ErrMsg(gle);
702                 Wait();
703                 return 0;
704             }
705             else if (isCmdWithArg(argc, argv, n, L"dumpmenu", arg))
706             {
707                 HRESULT hr;
708                 CComPtr<IContextMenu> cm;
709                 if (CLSIDPrefix(arg, g_CLSID))
710                 {
711                     g_ShellExtInit = arg;
712                     hr = LoadAndInitialize(IID_PPV_ARG(IContextMenu, &cm));
713                 }
714                 else
715                 {
716                     CComPtr<IShellItem> si;
717                     hr = CreateShellItemFromParse(arg, &si);
718                     if (SUCCEEDED(hr))
719                         hr = si->BindToHandler(NULL, BHID_SFUIObject, IID_PPV_ARG(IContextMenu, &cm));
720                 }
721                 if (SUCCEEDED(hr))
722                 {
723                     UINT first = 10, last = 9000;
724                     UINT cmf = 0, nosub = 0, fakeinit = 0;
725                     while (++n < argc)
726                     {
727                         if (argv[n][0] != '-' && argv[n][0] != '/')
728                             break;
729                         else if (isCmd(argc, argv, n, L"DEFAULTONLY"))
730                             cmf |= CMF_DEFAULTONLY;
731                         else if (isCmd(argc, argv, n, L"NODEFAULT"))
732                             cmf |= CMF_NODEFAULT;
733                         else if (isCmd(argc, argv, n, L"DONOTPICKDEFAULT"))
734                             cmf |= CMF_DONOTPICKDEFAULT;
735                         else if (isCmd(argc, argv, n, L"EXTENDED") || isCmd(argc, argv, n, L"EXTENDEDVERBS"))
736                             cmf |= CMF_EXTENDEDVERBS;
737                         else if (isCmd(argc, argv, n, L"SYNCCASCADEMENU"))
738                             cmf |= CMF_SYNCCASCADEMENU;
739                         else if (isCmd(argc, argv, n, L"EXPLORE"))
740                             cmf |= CMF_EXPLORE;
741                         else if (isCmd(argc, argv, n, L"VERBSONLY"))
742                             cmf |= CMF_VERBSONLY;
743                         else if (isCmd(argc, argv, n, L"NOVERBS"))
744                             cmf |= CMF_NOVERBS;
745                         else if (isCmd(argc, argv, n, L"DISABLEDVERBS"))
746                             cmf |= CMF_DISABLEDVERBS;
747                         else if (isCmd(argc, argv, n, L"OPTIMIZEFORINVOKE"))
748                             cmf |= CMF_OPTIMIZEFORINVOKE;
749                         else if (isCmd(argc, argv, n, L"CANRENAME"))
750                             cmf |= CMF_CANRENAME;
751                         else if (isCmd(argc, argv, n, L"NOSUBMENU"))
752                             nosub++;
753                         else if (isCmd(argc, argv, n, L"INITMENUPOPUP"))
754                             fakeinit++; // Tickle async submenus
755                         else
756                             wprintf(L"WARN: Ignoring switch %s\n", argv[n]);
757                     }
758                     HMENU hMenu = CreatePopupMenu();
759                     hr = cm->QueryContextMenu(hMenu, 0, first, last, cmf);
760                     if (SUCCEEDED(hr))
761                     {
762                         DumpMenu(hMenu, first, cm, fakeinit, nosub ? -1 : 0);
763                     }
764                     DestroyMenu(hMenu);
765                 }
766                 if (FAILED(hr))
767                     return ErrMsg(hr);
768                 return 0;
769             }
770             else if (isCmdWithArg(argc, argv, n, L"clsid", arg))
771             {
772                 HRESULT hr = CLSIDFromString(arg, &g_CLSID);
773                 if (!SUCCEEDED(hr))
774                 {
775                     wprintf(L"Failed to convert %s to CLSID\n", arg);
776                     failArgs = true;
777                 }
778             }
779             else if (isCmdWithArg(argc, argv, n, L"dll", arg))
780             {
781                 g_DLL = arg;
782             }
783             else if (isCmdWithArg(argc, argv, n, L"IShellExtInit", arg))
784             {
785                 g_ShellExtInit = arg;
786             }
787             else if (isCmd(argc, argv, n, L"IShellPropSheetExt"))
788             {
789                 g_bIShellPropSheetExt = true;
790             }
791             else if (isCmdWithArg(argc, argv, n, L"IContextMenu", arg))
792             {
793                 g_ContextMenu = arg;
794             }
795             else if (isCmd(argc, argv, n, L"infinite"))
796             {
797                 g_Wait = Wait_Infinite;
798             }
799             else if (isCmd(argc, argv, n, L"openwindows"))
800             {
801                 g_Wait = Wait_OpenWindows;
802             }
803             else if (isCmd(argc, argv, n, L"input"))
804             {
805                 g_Wait = Wait_Input;
806             }
807             else if (isCmd(argc, argv, n, L"explorerinstance"))
808             {
809                 g_Wait = Wait_ExplorerInstance;
810             }
811             else if (isCmd(argc, argv, n, L"nowait"))
812             {
813                 g_Wait = Wait_None;
814             }
815             else
816             {
817                 wprintf(L"Unknown argument: %s\n", cmd);
818                 failArgs = true;
819             }
820         }
821     }
822 
823     if (failArgs)
824     {
825         PrintHelp(NULL);
826         return E_INVALIDARG;
827     }
828 
829     CLSID EmptyCLSID = { 0 };
830     if (EmptyCLSID == g_CLSID)
831     {
832         PrintHelp(L"No CLSID specified");
833         return E_INVALIDARG;
834     }
835 
836     if (g_ShellExtInit.IsEmpty())
837     {
838         PrintHelp(L"No filename specified");
839         return E_INVALIDARG;
840     }
841 
842     HRESULT hr;
843     if (g_bIShellPropSheetExt)
844     {
845         CComPtr<IShellPropSheetExt> spSheetExt;
846         hr = LoadAndInitialize(IID_PPV_ARG(IShellPropSheetExt, &spSheetExt));
847         if (!SUCCEEDED(hr))
848             return hr;
849 
850         hr = spSheetExt->AddPages(cb_AddPage, (LPARAM)&g_Pages);
851         if (!SUCCEEDED(hr))
852         {
853             wprintf(L"IShellPropSheetExt->AddPages failed: 0x%x\n", hr);
854             return hr;
855         }
856 
857         USHORT ActivePage = HRESULT_CODE(hr);
858         PROPSHEETHEADERW psh = { 0 };
859 
860         psh.dwSize = sizeof(psh);
861         psh.dwFlags = PSH_PROPTITLE;
862         psh.pszCaption = L"shlextdbg";
863         psh.phpage = g_Pages.GetData();
864         psh.nPages = g_Pages.GetSize();
865         psh.nStartPage = ActivePage ? (ActivePage-1) : 0;
866         hr = PropertySheetW(&psh);
867 
868         wprintf(L"PropertySheetW returned: 0x%x\n", hr);
869     }
870     if (!g_ContextMenu.IsEmpty())
871     {
872         CComPtr<IContextMenu> spContextMenu;
873         hr = LoadAndInitialize(IID_PPV_ARG(IContextMenu, &spContextMenu));
874         if (!SUCCEEDED(hr))
875             return hr;
876 
877         // FIXME: Must call QueryContextMenu before InvokeCommand?
878 
879         CMINVOKECOMMANDINFO cm = { sizeof(cm), 0 };
880         cm.lpVerb = g_ContextMenu.GetString();
881         cm.nShow = SW_SHOW;
882         hr = spContextMenu->InvokeCommand(&cm);
883 
884         if (!SUCCEEDED(hr))
885         {
886             wprintf(L"IContextMenu->InvokeCommand failed: 0x%x\n", hr);
887             return hr;
888         }
889         wprintf(L"IContextMenu->InvokeCommand returned: 0x%x\n", hr);
890     }
891 
892     Wait();
893     return 0;
894 }
895