Question

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).

Was it helpful?

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.

OTHER TIPS

Have you tried using SHGetFileInfo?

There is another solution. First call GetShortPathName() and then GetLongPathName(). Guess what character case will be used then? ;-)

Okay, this is VBScript, but even so I'd suggest using the Scripting.FileSystemObject object

Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
Dim f
Set f = fso.GetFile("C:\testfile.dat") 'actually named "testFILE.dAt"
wscript.echo f.Name

The response I get is from this snippet is

testFILE.dAt

Hope that at least points you in the right direction.

Just found that the Scripting.FileSystemObject suggested by @bugmagnet 10 years ago is a treasure. Unlike my old method, it works on Absolute Path, Relative Path, UNC Path and Very Long Path (path longer than MAX_PATH). Shame on me for not testing his method earlier.

For future reference, I would like to present this code which can be compiled in both C and C++ mode. In C++ mode, the code will use STL and ATL. In C mode, you can clearly see how everything is working behind the scene.

#include <Windows.h>
#include <objbase.h>
#include <conio.h> // for _getch()

#ifndef __cplusplus
#   include <stdio.h>

#define SafeFree(p, fn) \
    if (p) { fn(p); (p) = NULL; }

#define SafeFreeCOM(p) \
    if (p) { (p)->lpVtbl->Release(p); (p) = NULL; }


static HRESULT CorrectPathCasing2(
    LPCWSTR const pszSrc, LPWSTR *ppszDst)
{
    DWORD const clsCtx = CLSCTX_INPROC_SERVER;
    LCID const lcid = LOCALE_USER_DEFAULT;
    LPCWSTR const pszProgId = L"Scripting.FileSystemObject";
    LPCWSTR const pszMethod = L"GetAbsolutePathName";
    HRESULT hr = 0;
    CLSID clsid = { 0 };
    IDispatch *pDisp = NULL;
    DISPID dispid = 0;
    VARIANT vtSrc = { VT_BSTR };
    VARIANT vtDst = { VT_BSTR };
    DISPPARAMS params = { 0 };
    SIZE_T cbDst = 0;
    LPWSTR pszDst = NULL;

    // CoCreateInstance<IDispatch>(pszProgId, &pDisp)

    hr = CLSIDFromProgID(pszProgId, &clsid);
    if (FAILED(hr)) goto eof;

    hr = CoCreateInstance(&clsid, NULL, clsCtx,
        &IID_IDispatch, (void**)&pDisp);
    if (FAILED(hr)) goto eof;
    if (!pDisp) {
        hr = E_UNEXPECTED; goto eof;
    }

    // Variant<BSTR> vtSrc(pszSrc), vtDst;
    // vtDst = pDisp->InvokeMethod( pDisp->GetIDOfName(pszMethod), vtSrc );

    hr = pDisp->lpVtbl->GetIDsOfNames(pDisp, NULL,
        (LPOLESTR*)&pszMethod, 1, lcid, &dispid);
    if (FAILED(hr)) goto eof;

    vtSrc.bstrVal = SysAllocString(pszSrc);
    if (!vtSrc.bstrVal) {
        hr = E_OUTOFMEMORY; goto eof;
    }
    params.rgvarg = &vtSrc;
    params.cArgs = 1;
    hr = pDisp->lpVtbl->Invoke(pDisp, dispid, NULL, lcid,
        DISPATCH_METHOD, &params, &vtDst, NULL, NULL);
    if (FAILED(hr)) goto eof;
    if (!vtDst.bstrVal) {
        hr = E_UNEXPECTED; goto eof;
    }

    // *ppszDst = AllocWStrCopyBStrFrom(vtDst.bstrVal);

    cbDst = SysStringByteLen(vtDst.bstrVal);
    pszDst = HeapAlloc(GetProcessHeap(),
        HEAP_ZERO_MEMORY, cbDst + sizeof(WCHAR));
    if (!pszDst) {
        hr = E_OUTOFMEMORY; goto eof;
    }
    CopyMemory(pszDst, vtDst.bstrVal, cbDst);
    *ppszDst = pszDst;

eof:
    SafeFree(vtDst.bstrVal, SysFreeString);
    SafeFree(vtSrc.bstrVal, SysFreeString);
    SafeFreeCOM(pDisp);
    return hr;
}

static void Cout(char const *psz)
{
    printf("%s", psz);
}

static void CoutErr(HRESULT hr)
{
    printf("Error HRESULT 0x%.8X!\n", hr);
}

static void Test(LPCWSTR pszPath)
{
    LPWSTR pszRet = NULL;
    HRESULT hr = CorrectPathCasing2(pszPath, &pszRet);
    if (FAILED(hr)) {
        wprintf(L"Input: <%s>\n", pszPath);
        CoutErr(hr);
    }
    else {
        wprintf(L"Was: <%s>\nNow: <%s>\n", pszPath, pszRet);
        HeapFree(GetProcessHeap(), 0, pszRet);
    }
}


#else // Use C++ STL and ATL
#   include <iostream>
#   include <iomanip>
#   include <string>
#   include <atlbase.h>

static HRESULT CorrectPathCasing2(
    std::wstring const &srcPath,
    std::wstring &dstPath)
{
    HRESULT hr = 0;
    CComPtr<IDispatch> disp;
    hr = disp.CoCreateInstance(L"Scripting.FileSystemObject");
    if (FAILED(hr)) return hr;

    CComVariant src(srcPath.c_str()), dst;
    hr = disp.Invoke1(L"GetAbsolutePathName", &src, &dst);
    if (FAILED(hr)) return hr;

    SIZE_T cch = SysStringLen(dst.bstrVal);
    dstPath = std::wstring(dst.bstrVal, cch);
    return hr;
}

static void Cout(char const *psz)
{
    std::cout << psz;
}

static void CoutErr(HRESULT hr)
{
    std::wcout
        << std::hex << std::setfill(L'0') << std::setw(8)
        << "Error HRESULT 0x" << hr << "\n";
}

static void Test(std::wstring const &path)
{
    std::wstring output;
    HRESULT hr = CorrectPathCasing2(path, output);
    if (FAILED(hr)) {
        std::wcout << L"Input: <" << path << ">\n";
        CoutErr(hr);
    }
    else {
        std::wcout << L"Was: <" << path << ">\n"
            << "Now: <" << output << ">\n";
    }
}

#endif


static void TestRoutine(void)
{
    HRESULT hr = CoInitialize(NULL);

    if (FAILED(hr)) {
        Cout("CoInitialize failed!\n");
        CoutErr(hr);
        return;
    }

    Cout("\n[ Absolute Path ]\n");
    Test(L"c:\\uSers\\RayMai\\docuMENTs");
    Test(L"C:\\WINDOWS\\SYSTEM32");

    Cout("\n[ Relative Path ]\n");
    Test(L".");
    Test(L"..");
    Test(L"\\");

    Cout("\n[ UNC Path ]\n");
    Test(L"\\\\VMWARE-HOST\\SHARED FOLDERS\\D\\PROGRAMS INSTALLER");

    Cout("\n[ Very Long Path ]\n");
    Test(L"\\\\?\\C:\\VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME\\"
        L"VERYVERYVERYLOOOOOOOONGFOLDERNAME");

    Cout("\n!! Worth Nothing Behavior !!\n");
    Test(L"");
    Test(L"1234notexist");
    Test(L"C:\\bad\\PATH");

    CoUninitialize();
}

int main(void)
{
    TestRoutine();
    _getch();
    return 0;
}

Screenshot:

screenshot2


Old Answer:

I found that FindFirstFile() will return the proper casing file name (last part of path) in fd.cFileName. If we pass c:\winDOWs\exPLORER.exe as first parameter to FindFirstFile(), the fd.cFileName would be explorer.exe like this:

prove

If we replace the last part of path with fd.cFileName, we will get the last part right; the path would become c:\winDOWs\explorer.exe.

Assuming the path is always absolute path (no change in text length), we can just apply this 'algorithm' to every part of path (except the drive letter part).

Talk is cheap, here is the code:

#include <windows.h>
#include <stdio.h>

/*
    c:\windows\windowsupdate.log --> c:\windows\WindowsUpdate.log
*/
static HRESULT MyProcessLastPart(LPTSTR szPath)
{
    HRESULT hr = 0;
    HANDLE hFind = NULL;
    WIN32_FIND_DATA fd = {0};
    TCHAR *p = NULL, *q = NULL;
    /* thePart = GetCorrectCasingFileName(thePath); */
    hFind = FindFirstFile(szPath, &fd);
    if (hFind == INVALID_HANDLE_VALUE) {
        hr = HRESULT_FROM_WIN32(GetLastError());
        hFind = NULL; goto eof;
    }
    /* thePath = thePath.ReplaceLast(thePart); */
    for (p = szPath; *p; ++p);
    for (q = fd.cFileName; *q; ++q, --p);
    for (q = fd.cFileName; *p = *q; ++p, ++q);
eof:
    if (hFind) { FindClose(hFind); }
    return hr;
}

/*
    Important! 'szPath' should be absolute path only.
    MUST NOT SPECIFY relative path or UNC or short file name.
*/
EXTERN_C
HRESULT __stdcall
CorrectPathCasing(
    LPTSTR szPath)
{
    HRESULT hr = 0;
    TCHAR *p = NULL;
    if (GetFileAttributes(szPath) == -1) {
        hr = HRESULT_FROM_WIN32(GetLastError()); goto eof;
    }
    for (p = szPath; *p; ++p)
    {
        if (*p == '\\' || *p == '/')
        {
            TCHAR slashChar = *p;
            if (p[-1] == ':') /* p[-2] is drive letter */
            {
                p[-2] = toupper(p[-2]);
                continue;
            }
            *p = '\0';
            hr = MyProcessLastPart(szPath);
            *p = slashChar;
            if (FAILED(hr)) goto eof;
        }
    }
    hr = MyProcessLastPart(szPath);
eof:
    return hr;
}

int main()
{
    TCHAR szPath[] = TEXT("c:\\windows\\EXPLORER.exe");
    HRESULT hr = CorrectPathCasing(szPath);
    if (SUCCEEDED(hr))
    {
        MessageBox(NULL, szPath, TEXT("Test"), MB_ICONINFORMATION);
    }
    return 0;
}

prove 2

Advantages:

  • The code works on every version of Windows since Windows 95.
  • Basic error-handling.
  • Highest performance possible. FindFirstFile() is very fast, direct buffer manipulation makes it even faster.
  • Just C and pure WinAPI. Small executable size.

Disadvantages:

  • Only absolute path is supported, other are undefined behavior.
  • Not sure if it is relying on undocumented behavior.
  • The code might be too raw too much DIY for some people. Might get you flamed.

Reason behind the code style:

I use goto for error-handling because I was used to it (goto is very handy for error-handling in C). I use for loop to perform functions like strcpy and strchr on-the-fly because I want to be certain what was actually executed.

FindFirstFileNameW will work with a few drawbacks:

  • it doesn't work on UNC paths
  • it strips the drive letter so you need to add it back
  • if there are more than one hard link to your file you need to identify the right one

After a quick test, GetLongPathName() does what you want.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top