How can I obtain the case-sensitive path on Windows?
Asked Answered
P

9

41

I need to know which is the real path of a given path.

For example:

The real path is: d:\src\File.txt
And the user give me: D:\src\file.txt
I need as a result: d:\src\File.txt

Posey answered 21/1, 2011 at 19:49 Comment(7)
What code is failing when using the path entered by the user? Paths typically are not case-sensitive on Windows.Solander
I'm under the impression that Windows has a fundamentally case-insensitive filesystem. That being the case, this is at best unnecessary, and at worst... nonsense. :)Mycobacterium
Are you sure you want to do that? What if the user gives you a path across a slow network, do you really want to verify it just to get the casing right?Rectocele
@djacobson: You're wrong. Windows is fundamentally case-sensitive, but certain flags have made it behave case-insensitively. Search for OBJ_CASE_INSENSITIVE for details. You might need a case-sensitive path, for example, if you're writing a BASH emulator, in which case you'd naturally need the correct casing for a file.Hoch
I need to apply the changes that has been made to a case-sensitive platform, so a I need to know the real path to look for on the other side.Posey
@Rodrigo: Yeah, your question is completely valid. I'll post up a longer (but more robust) solution that can work for everything.Hoch
@Mehrdad Ah, a little research proves you're correct. I stand corrected!Mycobacterium
S
19

You can use this function:

[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
static extern uint GetLongPathName(string ShortPath, StringBuilder sb, int buffer);

[DllImport("kernel32.dll")]
static extern uint GetShortPathName(string longpath, StringBuilder sb, int buffer); 

protected static string GetWindowsPhysicalPath(string path)
{
        StringBuilder builder = new StringBuilder(255);

        // names with long extension can cause the short name to be actually larger than
        // the long name.
        GetShortPathName(path, builder, builder.Capacity);

        path = builder.ToString();

        uint result = GetLongPathName(path, builder, builder.Capacity);

        if (result > 0 && result < builder.Capacity)
        {
            //Success retrieved long file name
            builder[0] = char.ToLower(builder[0]);
            return builder.ToString(0, (int)result);
        }

        if (result > 0)
        {
            //Need more capacity in the buffer
            //specified in the result variable
            builder = new StringBuilder((int)result);
            result = GetLongPathName(path, builder, builder.Capacity);
            builder[0] = char.ToLower(builder[0]);
            return builder.ToString(0, (int)result);
        }

        return null;
}
Subdelirium answered 21/1, 2011 at 19:51 Comment(10)
Have you checked this? I'm not saying it doesn't work, but I'm not sure it works either, since I doubt that it actually creates the file and changes the path casing.Hoch
Sorry at first I thought this doesn't work, but I was testing GetFullPathName and not GetLongPathName. Nice solution.Hoch
There are other alternatives, but I use this one for single paths.Subdelirium
Have you tested this on file systems with short names disabled?Ropedancer
@HarryJohnston: I tested it and it does not work, no error is thrown/returned but the GetShortPathName simply returns the long path in the case it was specified.Instable
works perfectly for me too. But, the drive letter is always lowercase. Example: on windows the path is: D:\Test\test.txt, the function returns d:\Test\test.txt.Theophany
This only works if the file actually has its short name set. As you can see in the documentation, that is not mandatory.Oberammergau
This method doesn't work in all cases. There's still something wrong.Ezzell
@Ezzell - did you find alternative solution? we having problems with long path of shared drive (e.g. \\machineName\Folder1\Folder2\Folder3\Folder4\Folder5Finegan
This doesn't seem to work when parts of the path are longer than 8 characters. For example, C:\ratherlongname\anotherlongname.txt won't work, but C:\short\short.txt will. This means it's not a general solution unfortunately.Sneak
D
10

As an old-timer, I always used FindFirstFile for this purpose. The .Net translation is:

Directory.GetFiles(Path.GetDirectoryName(userSuppliedName), Path.GetFileName(userSuppliedName)).FirstOrDefault();

This only gets you the correct casing for the filename portion of the path, not then entire path.

JeffreyLWhitledge's comment provides a link to a recursive version that can work (though not always) to resolve the full path.

Dornick answered 21/1, 2011 at 20:1 Comment(5)
nice; love the one liner without the dllimportsErrolerroll
This does not generate the correct output path that is desired.Brancusi
@Brancusi can you give a specific example where this fails?Dornick
Have you actually tried this? It doesn't work for me at all. Directory casing is still from userSuppliedName. Tried a couple of .NET versions, with the same result.Grainfield
@JeffreyLWhitledge take a look at this answer https://mcmap.net/q/393259/-c-filepath-recasingHalfhardy
P
5

Alternative Solution

Here is a solution that worked for me to move files between Windows and a server using case sensitive paths. It walks down the directory tree and corrects each entry with GetFileSystemEntries(). If part of the path is invalid (UNC or folder name), then it corrects the path only up to that point and then uses the original path for what it can't find. Anyway, hopefully this will save others time when dealing with the same issue.

private string GetCaseSensitivePath(string path)
{
    var root = Path.GetPathRoot(path);
    try
    {
        foreach (var name in path.Substring(root.Length).Split(Path.DirectorySeparatorChar))
            root = Directory.GetFileSystemEntries(root, name).First();
    }
    catch (Exception e)
    {
        // Log("Path not found: " + path);
        root += path.Substring(root.Length);
    }
    return root;
}
Portie answered 5/2, 2018 at 16:43 Comment(2)
This method does not check the file name casing. It is not a valid answer to the actual question.School
Works well, even with file name casing! +1 I suggest to use EnumerateFileSystemEntries instead of GetFileSystemEntries.Sully
H
3

The way to get the actual path of a file (this won't work for folders) is to follow these steps:

  1. Call CreateFileMapping to create a mapping for the file.
  2. Call GetMappedFileName to get the name of the file.
  3. Use QueryDosDevice to convert it to an MS-DOS-style path name.

If you feel like writing a more robust program that also works with directories (but with more pain and a few undocumented features), follow these steps:

  1. Get a handle to the file/folder with CreateFile or NtOpenFile.
  2. Call NtQueryObject to get the full path name.
  3. Call NtQueryInformationFile with FileNameInformation to get the volume-relative path.
  4. Using the two paths above, get the component of the path that represents the volume itself. For example, if you get \Device\HarddiskVolume1\Hello.txt for the first path and \Hello.txt for the second, you now know the volume's path is \Device\HarddiskVolume1.
  5. Use either the poorly-documented Mount Manager I/O Control Codes or QueryDosDevice to convert substitute the volume portion of the full NT-style path with the drive letter.

Now you have the real path of the file.

Hoch answered 21/1, 2011 at 19:54 Comment(2)
Presumably given a directory you could create a temporary file, use the first technique to get the actual path of the file, then strip off the filename part? (Well, if you've got write access, anyway.)Ropedancer
There's also GetFinalPathNameByHandle as of Windows Vista.Ropedancer
D
3

Here's an alternate solution, works on files and directories. Uses GetFinalPathNameByHandle, which is only supported for desktop apps on Vista/Server2008 or above according to docs.

Note that it will resolve a symlink if you give it one, which is part of finding the "final" path.

// http://www.pinvoke.net/default.aspx/shell32/GetFinalPathNameByHandle.html
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint GetFinalPathNameByHandle(SafeFileHandle hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags);
private const uint FILE_NAME_NORMALIZED = 0x0;

static string GetFinalPathNameByHandle(SafeFileHandle fileHandle)
{
    StringBuilder outPath = new StringBuilder(1024);

    var size = GetFinalPathNameByHandle(fileHandle, outPath, (uint)outPath.Capacity, FILE_NAME_NORMALIZED);
    if (size == 0 || size > outPath.Capacity)
        throw new Win32Exception(Marshal.GetLastWin32Error());

    // may be prefixed with \\?\, which we don't want
    if (outPath[0] == '\\' && outPath[1] == '\\' && outPath[2] == '?' && outPath[3] == '\\')
        return outPath.ToString(4, outPath.Length - 4);

    return outPath.ToString();
}

// http://www.pinvoke.net/default.aspx/kernel32.createfile
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern SafeFileHandle CreateFile(
     [MarshalAs(UnmanagedType.LPTStr)] string filename,
     [MarshalAs(UnmanagedType.U4)] FileAccess access,
     [MarshalAs(UnmanagedType.U4)] FileShare share,
     IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
     [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
     [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
     IntPtr templateFile);
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;

public static string GetFinalPathName(string dirtyPath)
{
    // use 0 for access so we can avoid error on our metadata-only query (see dwDesiredAccess docs on CreateFile)
    // use FILE_FLAG_BACKUP_SEMANTICS for attributes so we can operate on directories (see Directories in remarks section for CreateFile docs)

    using (var directoryHandle = CreateFile(
        dirtyPath, 0, FileShare.ReadWrite | FileShare.Delete, IntPtr.Zero, FileMode.Open,
        (FileAttributes)FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero))
    {
        if (directoryHandle.IsInvalid)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        return GetFinalPathNameByHandle(directoryHandle);
    }
}
Digger answered 31/12, 2016 at 15:44 Comment(1)
I tried like 5 other solutions, this is the first one that works for both D: and C: drive paths. Thank you!Landlady
I
2

As Borja's answer does not work for volumes where 8.3 names are disabled, here the recursive implementation that Tergiver suggests (works for files and folders, as well as the files and folders of UNC shares but not on their machine names nor their share names).

Non-existing file or folders are no problem, what exists is verified and corrected, but you might run into folder-redirection issues, e.g when trying to get the correct path of "C:\WinDoWs\sYsteM32\driVErs\eTC\Hosts" you'll get "C:\Windows\System32\drivers\eTC\hosts" on a 64bit windows as there is no "etc" folder withing "C:\Windows\sysWOW64\drivers".

Test Scenario:

        Directory.CreateDirectory(@"C:\Temp\SomeFolder");
        File.WriteAllLines(@"C:\Temp\SomeFolder\MyTextFile.txt", new String[] { "Line1", "Line2" });

Usage:

        FileInfo myInfo = new FileInfo(@"C:\TEMP\SOMEfolder\MyTeXtFiLe.TxT");
        String myResult = myInfo.GetFullNameWithCorrectCase(); //Returns "C:\Temp\SomeFolder\MyTextFile.txt"

Code:

public static class FileSystemInfoExt {

    public static String GetFullNameWithCorrectCase(this FileSystemInfo fileOrFolder) {
        //Check whether null to simulate instance method behavior
        if (Object.ReferenceEquals(fileOrFolder, null)) throw new NullReferenceException();
        //Initialize common variables
        String myResult = GetCorrectCaseOfParentFolder(fileOrFolder.FullName);
        return myResult;
    }

    private static String GetCorrectCaseOfParentFolder(String fileOrFolder) {
        String myParentFolder = Path.GetDirectoryName(fileOrFolder);
        String myChildName = Path.GetFileName(fileOrFolder);
        if (Object.ReferenceEquals(myParentFolder, null)) return fileOrFolder.TrimEnd(new char[]{Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
        if (Directory.Exists(myParentFolder)) {
            //myParentFolder = GetLongPathName.Invoke(myFullName);
            String myFileOrFolder = Directory.GetFileSystemEntries(myParentFolder, myChildName).FirstOrDefault();
            if (!Object.ReferenceEquals(myFileOrFolder, null)) {
                myChildName = Path.GetFileName(myFileOrFolder);
            }
        }
        return GetCorrectCaseOfParentFolder(myParentFolder) + Path.DirectorySeparatorChar + myChildName;
    }

}
Instable answered 20/4, 2015 at 15:13 Comment(0)
I
0

I tried to avoid dll imports so the best way for me was to use System.Linq and the System.IO.Directory class.

For your example Real path is: d:\src\File.txt The user give me: D:\src\file.txt

Code for this:

using System.Linq;

public static class PathUtils
{
    public static string RealPath(string inputPath)
    {
        return Directory.GetFiles(Path.GetDirectoryName(inputPath))
            .FirstOrDefault(p => String.Equals(Path.GetFileName(p), 
                Path.GetFileName(inputPath), StringComparison.OrdinalIgnoreCase));
    }
}

var p = PathUtils.RealPath(@"D:\src\file.txt");

Method should return the path "d:\src\File.txt" or "D:\src\File.txt".

Incapacious answered 28/2, 2020 at 6:58 Comment(1)
This only works in case insensitive OS because GetDirectoryName throws exception in case sensitive onesSchool
G
0

Here is how I do it. Originally, I used to depend on GetFinalPathNameByHandle which is very good, but unfortunately, some custom file systems don't support it (of course NTFS does). I also tried NtQueryObject with ObjectNameInformation but again, they don't necessarily report the original file name.

So here is another "manual" way:

public static string GetRealPath(string fullPath)
{
    if (fullPath == null)
        return null; // invalid

    var pos = fullPath.LastIndexOf(Path.DirectorySeparatorChar);
    if (pos < 0 || pos == (fullPath.Length - 1))
        return fullPath.ToUpperInvariant(); // drive letter

    var dirPath = fullPath.Substring(0, pos);
    var realPath = GetRealPath(dirPath); // go recursive, we want the final full path
    if (realPath == null)
        return null; // doesn't exist

    var dir = new DirectoryInfo(realPath);
    if (!dir.Exists)
        return null; // doesn't exist
    
    var fileName = fullPath.Substring(pos + 1);
    if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) // avoid wildcard calls
        return null;

    return dir.EnumerateFileSystemInfos(fileName).FirstOrDefault()?.FullName; // may return null
}
Grandfather answered 3/11, 2021 at 11:9 Comment(0)
H
-3

On Windows, paths are case-insensitive. So both paths are equally real.

If you want to get some kind of a path with canonical capitalization (i. e. how Windows thinks it should be capitalized), you can call FindFirstFile() with the path as a mask, then take the full name of the found file. If the path is invalid, then you won't get a canonical name, natually.

Hellen answered 21/1, 2011 at 19:53 Comment(1)
Only caveat is the returned path (at least in the C++ API) is not the full path, just the name of the file you queried. For example, if you queried FindFirstFile("C:/users/bob/desktop"), you will get back "Desktop", and not "C:/Users/bob/Desktop". You can deal with this by splitting the path at each directory separator and calling FindFirstFile() progressively on each valid sub path to construct your final answer, but you would have to consider the inefficiency of doing soMinefield

© 2022 - 2024 — McMap. All rights reserved.