Converting Short to Long Paths

Background

Win32 has a function called GetShortPathName that, as the name suggests, converts a long path to a short (or "mangled") path. The inverse conversion, short to long, is often required. At first glance, the function GetFullPathName looks to be just the thing, but a closer inspection reveals that all this function does is to merge a relative file name with the current directory. There is a suitable function, GetLongPathName, but this was introduced with Windows 98 and is unavailable on Windows 95 or Windows NT. 

 

A Common Solution

A common solution is to use the functionality provided by the shell. A short path can be parsed by the desktop folder, yielding the shell equivalent of a path - an ITEMIDLIST. This ITEMIDLIST can then be converted to a long path by calling SHGetPathFromIdList. There are a couple of problems with this approach. First, it is quite expensive. Shell32.dll must be loaded into the process, and the business of converting a path into an ITEMIDLIST and back is relatively slow. Secondly, it may not be 100% reliable. I have heard reports that conversion of UNC paths may fail. 

(Aside: the reason I originally looked for another method was that I needed to do path conversions from within the context of a system-wide hook. Such a hook may be called from within the context of a 16-bit application, which could have a very small stack - perhaps only a few kilobytes. Calling shell functions easily "blows" such a small stack, causing an access violation or random behaviour.)

 

An Alternative Solution

The functionality required to reimplement GetLongPathName is available via the function FindFirstFile. However, FindFirstFile only retrieves information about files and subdirectories within a directory, and will not directly convert fully qualified pathnames. Consequently, there is some programming effort in walking the pathname and converting each element in turn.

 

The Code

The following function aims to replicate the behaviour of the Win32 function GetLongPathName. In my testing, the behaviour was identical under success conditions (i.e. when the path exists). However, if the path is not valid then the value subsequently returned from GetLastError may differ.

 

#include <Windows.h>

#include <memory>

#include <string>

#include <algorithm>

 

DWORD ShortToLongPathName(

            LPCTSTR lpszShortPath,

            LPTSTR lpszLongPath,

            DWORD cchBuffer)

{

            // Catch null pointers.

            if (!lpszShortPath || !lpszLongPath)

            {

                        SetLastError(ERROR_INVALID_PARAMETER);

                        return 0;

            }

 

            // Check whether the input path is valid.

            if (0xffffffff == GetFileAttributes(lpszShortPath))

                        return 0;

 

            // Special characters.

            TCHAR const sep = _T('\\');

            TCHAR const colon = _T(':');

 

            // Make some short aliases for basic_strings of TCHAR.

            typedef std::basic_string<TCHAR> tstring;

            typedef tstring::traits_type traits;

            typedef tstring::size_type size;

            size const npos = tstring::npos;

 

            // Copy the short path into the work buffer and convert forward

            // slashes to backslashes.

            tstring path = lpszShortPath;

            std::replace(path.begin(), path.end(), _T('/'), sep);

 

            // We need a couple of markers for stepping through the path.

            size left = 0;

            size right = 0;

 

            // Parse the first bit of the path.

            if (path.length() >= 2 && isalpha(path[0]) && colon == path[1]) // Drive letter?

            {

                        if (2 == path.length()) // 'bare' drive letter

                        {

                                    right = npos; // skip main block

                        }

                        else if (sep == path[2]) // drive letter + backslash

                        {

                                    // FindFirstFile doesn't like "X:\"

                                    if (3 == path.length())

                                    {

                                                right = npos; // skip main block

                                    }

                                    else

                                    {

                                                left = right = 3;

                                    }

                        }

                        else return 0; // parsing failure

            }

            else if (path.length() >= 1 && sep == path[0])

            {

                        if (1 == path.length()) // 'bare' backslash

                        {

                                    right = npos;  // skip main block

                        }

                        else

                        {

                                    if (sep == path[1]) // is it UNC?

                                    {

                                                // Find end of machine name

                                                right = path.find_first_of(sep, 2);

                                                if (npos == right)

                                                            return 0;

 

                                                // Find end of share name

                                                right = path.find_first_of(sep, right + 1);

                                                if (npos == right)

                                                            return 0;

                                    }

                                    ++right;

                        }

            }

            // else FindFirstFile will handle relative paths

 

            // The data block for FindFirstFile.

            WIN32_FIND_DATA fd;

 

            // Main parse block - step through path.

            while (npos != right)

            {

                        left = right; // catch up

 

                        // Find next separator.

                        right = path.find_first_of(sep, right);

 

                        // Is it "." or ".."?

                        tstring next = path.substr(left, (npos != right) ? right - left : npos);

                        if (next == tstring(_T(".")) || next == tstring(_T("..")))

                        {

                                    if (npos != right)

                                                ++right;

                        }

                        else // no

                        {

                                    // Temporarily replace the separator with a null character so that

                                    // the path so far can be passed to FindFirstFile.

                                    if (npos != right)

                                                path[right] = 0;

 

                                    // See what FindFirstFile makes of the path so far.

                                    HANDLE hf = FindFirstFile(path.c_str(), &fd);

                                    if (INVALID_HANDLE_VALUE == hf)

                                                return 0;

                                    FindClose(hf);

 

                                    // Put back the separator.

                                    if (npos != right)

                                                path[right] = sep;

 

                                    // The file was found - replace the short name with the long.

                                    size old_len = (npos == right) ? path.length() - left : right - left;

                                    size new_len = traits::length(fd.cFileName);

                                    path.replace(left, old_len, fd.cFileName, new_len);

 

                                    // More to do?

                                    if (npos != right)

                                    {

                                                // Yes - move past separator .

                                                right = left + new_len + 1;

 

                                                // Did we overshoot the end? (i.e. path ends with a separator).

                                                if (right >= path.length())

                                                            right = npos;

                                    }

                        }

            }

 

            // If buffer is too small then return the required size.

            if (cchBuffer <= path.length())

                        return path.length() + 1;

 

            // Copy the buffer and return the number of characters copied.

            traits::copy(lpszLongPath, path.c_str(), path.length() + 1);

                        return path.length();

}

 

Let me know whether this code works for you. If you spot an error then please tell me, so that I can fix this page. Cheers!

 

Home

Last updated 24 August 2007.

 

1