c++cwindowswinapi

Getting actual file name (with proper casing) on Windows


Windows file system is case insensitive. How, given a file/folder name (e.g. "somefile"), I get the actual name of that file/folder (e.g. it should return "SomeFile" if Explorer displays it so)?

Some ways I know, all of which seem quite backwards:

  1. Given the full path, search for each folder on the path (via FindFirstFile). This gives proper cased results of each folder. At the last step, search for the file itself.
  2. Get filename from handle (as in MSDN example). This requires opening a file, creating file mapping, getting it's name, parsing device names etc. Pretty convoluted. And it does not work for folders or zero-size files.

Am I missing some obvious WinAPI call? The simplest ones, like GetActualPathName() or GetFullPathName() return the name using casing that was passed in (e.g. returns "program files" if that was passed in, even if it should be "Program Files").

I'm looking for a native solution (not .NET one).


Solution

  • And hereby I answer my own question, based on original answer from cspirz.

    Here's a function that given absolute, relative or network path, will return the path with upper/lower case as it would be displayed on Windows. If some component of the path does not exist, it will return the passed in path from that point.

    It is quite involved because it tries to handle network paths and other edge cases. It operates on wide character strings and uses std::wstring. Yes, in theory Unicode TCHAR could be not the same as wchar_t; that is an exercise for the reader :)

    std::wstring GetActualPathName( const wchar_t* path )
    {
        // This is quite involved, but the meat is SHGetFileInfo
    
        const wchar_t kSeparator = L'\\';
    
        // copy input string because we'll be temporary modifying it in place
        size_t length = wcslen(path);
        wchar_t buffer[MAX_PATH];
        memcpy( buffer, path, (length+1) * sizeof(path[0]) );
    
        size_t i = 0;
    
        std::wstring result;
    
        // for network paths (\\server\share\RestOfPath), getting the display
        // name mangles it into unusable form (e.g. "\\server\share" turns
        // into "share on server (server)"). So detect this case and just skip
        // up to two path components
        if( length >= 2 && buffer[0] == kSeparator && buffer[1] == kSeparator )
        {
            int skippedCount = 0;
            i = 2; // start after '\\'
            while( i < length && skippedCount < 2 )
            {
                if( buffer[i] == kSeparator )
                    ++skippedCount;
                ++i;
            }
    
            result.append( buffer, i );
        }
        // for drive names, just add it uppercased
        else if( length >= 2 && buffer[1] == L':' )
        {
            result += towupper(buffer[0]);
            result += L':';
            if( length >= 3 && buffer[2] == kSeparator )
            {
                result += kSeparator;
                i = 3; // start after drive, colon and separator
            }
            else
            {
                i = 2; // start after drive and colon
            }
        }
    
        size_t lastComponentStart = i;
        bool addSeparator = false;
    
        while( i < length )
        {
            // skip until path separator
            while( i < length && buffer[i] != kSeparator )
                ++i;
    
            if( addSeparator )
                result += kSeparator;
    
            // if we found path separator, get real filename of this
            // last path name component
            bool foundSeparator = (i < length);
            buffer[i] = 0;
            SHFILEINFOW info;
    
            // nuke the path separator so that we get real name of current path component
            info.szDisplayName[0] = 0;
            if( SHGetFileInfoW( buffer, 0, &info, sizeof(info), SHGFI_DISPLAYNAME ) )
            {
                result += info.szDisplayName;
            }
            else
            {
                // most likely file does not exist.
                // So just append original path name component.
                result.append( buffer + lastComponentStart, i - lastComponentStart );
            }
    
            // restore path separator that we might have nuked before
            if( foundSeparator )
                buffer[i] = kSeparator;
    
            ++i;
            lastComponentStart = i;
            addSeparator = true;
        }
    
        return result;
    }
    

    Again, thanks to cspirz for pointing me to SHGetFileInfo.