Getting actual file name (with proper casing) on Windows

前端 未结 7 999
暗喜
暗喜 2020-12-05 19:29

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 \"SomeF

7条回答
  •  北海茫月
    2020-12-05 20:10

    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 
    #include 
    #include  // for _getch()
    
    #ifndef __cplusplus
    #   include 
    
    #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(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 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, ¶ms, &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 
    #   include 
    #   include 
    #   include 
    
    static HRESULT CorrectPathCasing2(
        std::wstring const &srcPath,
        std::wstring &dstPath)
    {
        HRESULT hr = 0;
        CComPtr 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:


    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:

    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 
    #include 
    
    /*
        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;
    }
    

    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.

提交回复
热议问题