c++winapiwindows-shellshell32

How to get filename with extension in shell envirionment (winapi)?


I have a program that gets information about files in the windows recycle bin. The only non-crutch solution to the bucket access problem I see is using the shell environment.

I have a code for removing single file (or folder) from recycle bin with winapi (shell envirionment) like the next:

#include <iostream>

#include <shobjidl_core.h>
#include <Shlobj.h>
#include <shlwapi.h>
#include <ntquery.h>

const SHCOLUMNID SCID_RemovedFrom = { PSGUID_DISPLACED, PID_DISPLACED_FROM };
const SHCOLUMNID SCID_RemovedName = { PSGUID_STORAGE, PID_STG_NAME };

const SHCOLUMNID SCID_DateDeleted = { PSGUID_DISPLACED, PID_DISPLACED_DATE };
const SHCOLUMNID SCID_DateCreated = { PSGUID_STORAGE, PID_STG_CREATETIME };
const SHCOLUMNID SCID_DateModifed = { PSGUID_STORAGE, PID_STG_WRITETIME };
const SHCOLUMNID SCID_DateOpened = { PSGUID_STORAGE, PID_STG_ACCESSTIME };

int main() {
    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

    LPSHELLFOLDER pDesktop = NULL;
    SHGetDesktopFolder(&pDesktop);

    LPITEMIDLIST pidlRecycleBin = NULL;
    SHGetSpecialFolderLocation(NULL, CSIDL_BITBUCKET, &pidlRecycleBin);

    IShellFolder2 *pRecycleBin;
    pDesktop->BindToObject(pidlRecycleBin, NULL, IID_IShellFolder2, (LPVOID*)&pRecycleBin);

    pDesktop->Release();
    CoTaskMemFree(pidlRecycleBin);

    IEnumIDList* penumFiles;
    pRecycleBin->EnumObjects(NULL, SHCONTF_FOLDERS | SHCONTF_NONFOLDERS, &penumFiles);
    
    STRRET sret;

    IFileOperation *pfo;
    CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pfo));
    pfo->SetOperationFlags(FOF_NO_UI);

    LPITEMIDLIST pidl = NULL;

    BSTR bstr = NULL;
    VARIANT vt;
    SYSTEMTIME syst;

    while (penumFiles->Next(1, &pidl, NULL) != S_FALSE) {
        
        pRecycleBin->GetDisplayNameOf(pidl, SHGDN_NORMAL, &sret); // not a FULL name!!!

        StrRetToBSTR(&sret, pidl, &bstr);
        std::wcout << bstr << L' ';
        SysFreeString(bstr);

        pRecycleBin->GetDetailsEx(pidl, &SCID_DateDeleted, &vt);
        VariantTimeToSystemTime(vt.date, &syst);
        std::wcout << "\t" << syst.wHour << ":" << syst.wMinute << " " << syst.wDay << "." << syst.wMonth << "." << syst.wYear << std::endl;

        /*MARK ITEMS TO DELETE*/
        SHCreateItemWithParent(NULL, pRecycleBin, pidl, IID_IShellItem, (void**)&shi);
        pfo->DeleteItem(shi, NULL);
        shi->Release();

        CoTaskMemFree(pidl);
    }
    penumFiles->Release();
    pRecycleBin->Release();

    /*DELETE MARKED ITEMS*/
    pfo->PerformOperations();
    pfo->Release();

    //Update recycle bin icon [undocumented function]
    (void (*)())GetProcAddress(GetModuleHandle(L"shell32.dll"), "SHUpdateRecycleBinIcon")();

    CoUninitialize();
    return 0;
}

In the code above, at the top, there are constant definitions (SCID_RemovedFrom, SCID_RemovedName) for getting the path where the deleted file was located and the name via the function GetDetailsEx. However, the resulting name does not always contain the extension, but only when the file is registered in the system in such a way that the extension is displayed in File Explorer. In other cases, the name is obtained without an extension.

I couldn't find a constant where GetDetailsEx will return the full name (with the extension) regardless of the user's settings. So far, I've only found a crutch: use pRecycleBin->GetDisplayNameOf(pidl, SHGDN_FORPARSING, &sret); to get extensions, BUT there a problem: I can't determine how to interpret the name some.docx and extension $RXZH1I6.docx: is some.docx a full name (if extensions are shown) or some.docx.docx is a full name (if extensions are hidden). It turns out that this crutch is even dangerous.

How can I get the name of any file ("NONFOLDER") guaranteed to have an extension?


Solution

  • Here is some sample code (using ATL for smart pointers) that displays all properties for all items in the recycle bin (or any other folder you'd like).

    You can pick the properties you want. I've dumped a sample output with a deleted txt file. The item's display name actually depends on the maching settings, but you can see there are lots of other interesting properties, for example:

    Sample output:

    D:\temp\New Text Document.txt // this is the normal display name (depends on settings)
     System.ItemFolderNameDisplay = Recycle Bin
     System.ItemTypeText = Text Document
     System.ItemNameDisplay = New Text Document.txt
     System.Size = 0
     System.FileAttributes = 33
     System.DateModified = 2019/12/04:17:52:05.132
     System.DateCreated = 2019/12/04:17:52:06.000
     System.DateAccessed = 2019/12/04:17:52:06.000
     System.ItemNameDisplayWithoutExtension = $RWLK87M
     System.ContentType = text/plain
     System.Document.DateCreated = 2019/12/04:17:52:05.132
     System.Document.DateSaved = 2019/12/04:17:52:05.132
     System.Recycle.DeletedFrom = D:\temp
     System.Recycle.DateDeleted = 2021/02/16:08:18:46.000
     System.FileOwner = KILLROY\WasHere
     System.NetworkLocation = 
     System.ComputerName = KILLROY
     System.ItemPathDisplayNarrow = $RWLK87M (D:\$RECYCLE.BIN\Recycle Bin)
     System.PerceivedType = 1
     System.ItemType = .txt
     System.ParsingName = D:\$RECYCLE.BIN\S-1-5-21-2804694453-2728412037-1988799983-1001\$RWLK87M.txt
     System.SFGAOFlags = 1077936503
     System.ParsingPath = D:\$RECYCLE.BIN\S-1-5-21-2804694453-2728412037-1988799983-1001\$RWLK87M.txt
     System.FileExtension = .txt
     System.ItemDate = 2019/12/04:17:52:05.132
     System.KindText = Document
     System.FileAttributesDisplay = Read-only
     System.IsShared = 0
     System.SharedWith = 
     System.SharingStatus = 2
     System.ShareScope = 
     System.Security.EncryptionOwnersDisplay = 
     System.ItemName = $RWLK87M.txt
     System.Shell.SFGAOFlagsStrings = filesys; stream
     System.Link.TargetSFGAOFlagsStrings = 
     System.OfflineAvailability = 
     System.ZoneIdentifier = 0
     System.LastWriterPackageFamilyName = 
     System.AppZoneIdentifier = 
     System.Kind = document
     System.Security.EncryptionOwners = 
     System.ItemFolderPathDisplayNarrow = Recycle Bin (D:\$RECYCLE.BIN)
     System.FileName = New Text Document.txt
     System.Security.AllowedEnterpriseDataProtectionIdentities = 
     System.ThumbnailCacheId = 11524143073100486861
     System.VolumeId = {184CD9C9-570E-483F-9AE1-68D57270A239}
     System.Link.TargetParsingPath = 
     System.Link.TargetSFGAOFlags = 
     System.ItemFolderPathDisplay = D:\$RECYCLE.BIN\Recycle Bin
     System.ItemPathDisplay = D:\$RECYCLE.BIN\Recycle Bin\$RWLK87M.txt
     {9E5E05AC-1936-4A75-94F7-4704B8B01923} 0 = New Text Document.txt
     System.AppUserModel.ID = 
     System.AppUserModel.ParentID = 
     System.Link.TargetExtension = 
     System.OfflineStatus = 
     System.IsFolder = 0
     System.NotUserContent = 0
     System.StorageProviderAggregatedCustomStates = 
     System.SyncTransferStatusFlags = 
     System.DateImported = 2019/12/04:17:52:05.132
     System.ExpandoProperties = 
     System.FilePlaceholderStatus = 6
    

    Sample code:

    #include <iostream>
    #include <shobjidl_core.h>
    #include <shlobj.h>
    #include <propvarutil.h>
    #include <atlbase.h>
    #include <atlcom.h>
    
    #pragma comment(lib, "propsys")
    
    int main() {
        HRESULT hr = CoInitialize(nullptr);
        {
            // get recycle bin folder
            CComPtr<IShellItem> bin;
            hr = SHCreateItemInKnownFolder(FOLDERID_RecycleBinFolder, 0, nullptr, IID_PPV_ARGS(&bin));
            if (SUCCEEDED(hr))
            {
                // enumerate items
                CComPtr<IEnumShellItems> items;
                hr = bin->BindToHandler(nullptr, BHID_EnumItems, IID_PPV_ARGS(&items));
                if (SUCCEEDED(hr))
                {
                    do
                    {
                        // get item
                        CComPtr<IShellItem> item;
                        hr = items->Next(1, &item, nullptr);
                        if (hr != 0)
                            break;
    
                        // get the display name (depends on settings)
                        CComHeapPtr<wchar_t> path;
                        item->GetDisplayName(SIGDN_NORMALDISPLAY, &path);
                        std::wcout << path.m_pData << std::endl;
    
                        // get item's property store
                        CComPtr<IPropertyStore> store;
                        hr = CComQIPtr<IShellItem2>(item)->GetPropertyStore(GPS_DEFAULT, IID_PPV_ARGS(&store));
                        if (SUCCEEDED(hr))
                        {
                            DWORD count = 0;
                            store->GetCount(&count);
                            for (DWORD i = 0; i < count; i++)
                            {
                                // print this property (name, value)
                                PROPERTYKEY pk;
                                hr = store->GetAt(i, &pk);
                                if (SUCCEEDED(hr))
                                {
                                    // get property canonical name
                                    CComHeapPtr<wchar_t> name;
                                    PSGetNameFromPropertyKey(pk, &name);
    
                                    // get property value
                                    PROPVARIANT pv;
                                    PropVariantInit(&pv);
                                    hr = store->GetValue(pk, &pv);
                                    if (SUCCEEDED(hr))
                                    {
                                        CComHeapPtr<wchar_t> value;
                                        hr = PropVariantToStringAlloc(pv, &value); // propvarutil.h
                                        if (SUCCEEDED(hr))
                                        {
                                            if (!name) // unknown name
                                            {
                                                CComHeapPtr<wchar_t> fmtid;
                                                StringFromCLSID(pk.fmtid, &fmtid);
                                                std::wcout << L" " << fmtid.m_pData << L" " << pk.pid << L" = " << value.m_pData << std::endl;
                                            }
                                            else
                                            {
                                                std::wcout << L" " << name.m_pData << L" = " << value.m_pData << std::endl;
                                            }
                                        }
                                        else
                                        {
                                            // can't convert to string, print something useful
                                        }
                                    }
                                    PropVariantClear(&pv);
                                }
                            }
                        }
                    } while (true);
                }
            }
        }
        CoUninitialize();
        return 0;
    }