How to check whether 2 DirectoryInfo objects are pointing to the same directory?
Asked Answered
L

7

15

I have 2 DirectoryInfo objects and want to check if they are pointing to the same directory. Besides comparing their Fullname, are there any other better ways to do this? Please disregard the case of links.

Here's what I have.

DirectoryInfo di1 = new DirectoryInfo(@"c:\temp");
DirectoryInfo di2 = new DirectoryInfo(@"C:\TEMP");

if (di1.FullName.ToUpperInvariant() == di2.FullName.ToUpperInvariant())
{ // they are the same
   ...   
}

Thanks.

Labelle answered 25/11, 2009 at 1:0 Comment(1)
All of the answers below will give incorrect results in certain cases, i.e. they are all wrong. See https://mcmap.net/q/21981/-how-can-i-compare-directory-paths-in-c for a correct answer.Inclinable
C
6

Under Linux you could compare the INode numbers of the two files wheather they are identical. But under Windows there is no such concept, at least not that I know off. You would need to use p/invoke to resolve the links if any.

Comparing strings is the best you can do. Note that using String.Compare(str1, str2, StringComparison.OrdinalIgnoreCase) is faster than your approach of ToUpperInvariant() as it doesn't allocate new strings on the heap and won't suffer problems inherent in using a linguistic text comparison algorithm to compare file paths.

Callow answered 25/11, 2009 at 1:39 Comment(5)
What's wrong with this answered? Why was it marked down? I find the tip of using String.Compare() good.Labelle
This is basically the same as the solution provided in the question. The better approach is to use Uri, which will handle different formatting, etc.Davon
"But under Windows there is no such concept": This is wrong, hence this answer is essentially wrong. While INode in NFS is more reliable (guaranteed, in fact), windows can be coerced in to giving you similar information. See this answer in a related post: https://mcmap.net/q/21981/-how-can-i-compare-directory-paths-in-c.Inclinable
If you are looking to determine if two directory info objects refer to the same directory, this will not do the trick. It should be pointed out that it is incredibly difficult to determine if two path names refer to the same file system object, given the possibility of junctions, links, shares on networks, etc.Inclinable
InvariantCultureIgnoreCase should generally be avoided - always prefer OrdinalIgnoreCase instead, especially in filesystems as file-names are not linguistic strings, see learn.microsoft.com/en-us/dotnet/standard/base-types/…Ligniform
A
6

Since netstandard2.1 there is finally an almost convenient and platform-independent way to check this: Path.GetRelativePath().

var areEqual = Path.GetRelativePath(path1, path2) == ".";

Works with absolute and relative paths. Also handles cases like foo/../foo/bar == foo/bar correctly.

Aftmost answered 30/3, 2021 at 19:17 Comment(1)
I now see you have a deleted answer on the other question because people don't like duplicate answers; but anyhow - this is certainly viable!Photomultiplier
D
5

You can use Uri objects instead. However, your Uri objects must point to a "file" inside these directories. That file doesn't actually have to exist.

    private void CompareStrings()
    {
        string path1 = @"c:\test\rootpath";
        string path2 = @"C:\TEST\..\TEST\ROOTPATH";
        string path3 = @"C:\TeSt\RoOtPaTh\";

        string file1 = Path.Combine(path1, "log.txt");
        string file2 = Path.Combine(path2, "log.txt");
        string file3 = Path.Combine(path3, "log.txt");

        Uri u1 = new Uri(file1);
        Uri u2 = new Uri(file2);
        Uri u3 = new Uri(file3);

        Trace.WriteLine(string.Format("u1 == u2 ? {0}", u1 == u2));
        Trace.WriteLine(string.Format("u2 == u3 ? {0}", u2 == u3));

    }

This will print out:

u1 == u2 ? True
u2 == u3 ? True
Davon answered 11/2, 2010 at 21:52 Comment(3)
This should actually be the answer as it covers edge-cases like "\..\" and is more robust, although it needs a little workaroundTallu
but there is one problem, it translates stuff like %51 into letters instead of leaving them, so if you try this paths: @"c:\test\rootQpath" @"C:\TEST\..\TEST\ROOT%51PATH" It will return trueTallu
While C:\ will work, this will throw a UriFormatException if path is in the form of C: (ommiting the \ ) as Path.Combine will result in something like this C:log.txtUrbanite
T
4

Inspired from here:

static public bool SameDirectory(string path1, string path2)
{
    return (
        0 == String.Compare(
            System.IO.Path.GetFullPath(path1).TrimEnd('\\'),
            System.IO.Path.GetFullPath(path2).TrimEnd('\\'),
            StringComparison.InvariantCultureIgnoreCase))
        ;
}    

Works for files too...

(BTW theoretically questions are duplicate, but this is the original and the other one is the most answered one...)

HTH

Tippett answered 25/7, 2011 at 10:54 Comment(0)
A
0

Case-insensitive comparison is the best you can get. Extract it to a helper class just in case mankind comes up with a better method.

public static class DirectoryInfoExtensions
{
    public static bool IsEqualTo(this DirectoryInfo left, DirectoryInfo right)
    {
        return left.FullName.ToUpperInvariant() == right.FullName.ToUpperInvariant();
    }
}

and use:

if (di1.IsEqualTo(di2))
{
    // Code here
}
Antinucleon answered 25/11, 2009 at 2:58 Comment(0)
B
-1

Some extension methods that I wrote for a recent project includes one that will do it:

    public static bool IsSame(this DirectoryInfo that, DirectoryInfo other)
    {
        // zip extension wouldn't work here because it truncates the longer 
        // enumerable, resulting in false positive

        var e1 = that.EnumeratePathDirectories().GetEnumerator();
        var e2 = other.EnumeratePathDirectories().GetEnumerator();

        while (true)
        {
            var m1 = e1.MoveNext();
            var m2 = e2.MoveNext();
            if (m1 != m2) return false; // not same length
            if (!m1) return true; // finished enumerating with no differences found

            if (!e1.Current.Name.Trim().Equals(e2.Current.Name.Trim(), StringComparison.InvariantCultureIgnoreCase))
                return false; // current folder in paths differ
        }
    }

    public static IEnumerable<DirectoryInfo> EnumeratePathDirectories(this DirectoryInfo di)
    {
        var stack = new Stack<DirectoryInfo>();

        DirectoryInfo current = di;

        while (current != null)
        {
            stack.Push(current);
            current = current.Parent;
        }

        return stack;
    }

    // irrelevant for this question, but still useful:

    public static bool IsSame(this FileInfo that, FileInfo other)
    {
        return that.Name.Trim().Equals(other.Name.Trim(), StringComparison.InvariantCultureIgnoreCase) &&
               that.Directory.IsSame(other.Directory);
    }

    public static IEnumerable<DirectoryInfo> EnumeratePathDirectories(this FileInfo fi)
    {
        return fi.Directory.EnumeratePathDirectories();
    }

    public static bool StartsWith(this FileInfo fi, DirectoryInfo directory)
    {
        return fi.Directory.StartsWith(directory);
    }

    public static bool StartsWith(this DirectoryInfo di, DirectoryInfo directory)
    {
        return di.EnumeratePathDirectories().Any(d => d.IsSame(directory));
    }
Banana answered 23/4, 2013 at 15:13 Comment(0)
I
-1

If one actually wants to know when two paths resolve to the same file system object, you must do some IO. Trying to get two "normalized" names, that take in to account the myriad of possible ways of referencing the same file object, is next to impossible. There are issues such as: junctions, symbolic links, network file shares (referencing the same file object in different manners), etc. etc. *

If you are going to deal with networked paths, you will absolutely need to do IO: there are cases where it is simply not possible to determine from any local path-string manipulation, whether two file references will reference the same physical file. (This can be easily understood as follows. Suppose a file server has a windows directory junction somewhere within a shared subtree. In this case, a file can be referenced either directly, or through the junction. But the junction resides on, and is resolved by, the file server, and so it is simply impossible for a client to determine, purely through local information, that the two referencing file names refer to the same physical file: the information is simply not available locally to the client. Thus one must absolutely do some minimal IO - e.g. open two file object handles - to determine if the references refer to the same physical file.)

The following solution does some IO, though very minimal, but correctly determines whether two file system references are semantically identical, i.e. reference the same file object. (if neither file specification refers to a valid file object, all bets are off):

public static bool AreDirsEqual(string dirName1, string dirName2, bool resolveJunctionaAndNetworkPaths = true)
{
    if (string.IsNullOrEmpty(dirName1) || string.IsNullOrEmpty(dirName2))
        return dirName1==dirName2;
    dirName1 = NormalizePath(dirName1); //assume NormalizePath normalizes/fixes case and path separators to Path.DirectorySeparatorChar
    dirName2 = NormalizePath(dirName2);
    int i1 = dirName1.Length;
    int i2 = dirName2.Length;
    do
    {
        --i1; --i2;
        if (i1 < 0 || i2 < 0)
            return i1 < 0 && i2 < 0;
    } while (dirName1[i1] == dirName2[i2]);//If you want to deal with international character sets, i.e. if NormalixePath does not fix case, this comparison must be tweaked
    if( !resolveJunctionaAndNetworkPaths )
        return false;
    for(++i1, ++i2; i1 < dirName1.Length; ++i1, ++i2)
    {
        if (dirName1[i1] == Path.DirectorySeparatorChar)
        {
            dirName1 = dirName1.Substring(0, i1);
            dirName2 = dirName1.Substring(0, i2);
            break;
        }
    }
    return AreFileSystemObjectsEqual(dirName1, dirName2);
}

public static bool AreFileSystemObjectsEqual(string dirName1, string dirName2)
{
    //NOTE: we cannot lift the call to GetFileHandle out of this routine, because we _must_
    // have both file handles open simultaneously in order for the objectFileInfo comparison
    // to be guaranteed as valid.
    using (SafeFileHandle directoryHandle1 = GetFileHandle(dirName1), directoryHandle2 = GetFileHandle(dirName2))
    {
        BY_HANDLE_FILE_INFORMATION? objectFileInfo1 = GetFileInfo(directoryHandle1);
        BY_HANDLE_FILE_INFORMATION? objectFileInfo2 = GetFileInfo(directoryHandle2);
        return objectFileInfo1 != null
                && objectFileInfo2 != null
                && (objectFileInfo1.Value.FileIndexHigh == objectFileInfo2.Value.FileIndexHigh)
                && (objectFileInfo1.Value.FileIndexLow == objectFileInfo2.Value.FileIndexLow)
                && (objectFileInfo1.Value.VolumeSerialNumber == objectFileInfo2.Value.VolumeSerialNumber);
    }
}

static SafeFileHandle GetFileHandle(string dirName)
{
    const int FILE_ACCESS_NEITHER = 0;
    //const int FILE_SHARE_READ = 1;
    //const int FILE_SHARE_WRITE = 2;
    //const int FILE_SHARE_DELETE = 4;
    const int FILE_SHARE_ANY = 7;//FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
    const int CREATION_DISPOSITION_OPEN_EXISTING = 3;
    const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
    return CreateFile(dirName, FILE_ACCESS_NEITHER, FILE_SHARE_ANY, System.IntPtr.Zero, CREATION_DISPOSITION_OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, System.IntPtr.Zero);
}


static BY_HANDLE_FILE_INFORMATION? GetFileInfo(SafeFileHandle directoryHandle)
{
    BY_HANDLE_FILE_INFORMATION objectFileInfo;
    if ((directoryHandle == null) || (!GetFileInformationByHandle(directoryHandle.DangerousGetHandle(), out objectFileInfo)))
    {
        return null;
    }
    return objectFileInfo;
}

[DllImport("kernel32.dll", EntryPoint = "CreateFileW", CharSet = CharSet.Unicode, SetLastError = true)]
static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, int dwShareMode,
 IntPtr SecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetFileInformationByHandle(IntPtr hFile, out BY_HANDLE_FILE_INFORMATION lpFileInformation);

[StructLayout(LayoutKind.Sequential)]
public struct BY_HANDLE_FILE_INFORMATION
{
    public uint FileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
    public uint VolumeSerialNumber;
    public uint FileSizeHigh;
    public uint FileSizeLow;
    public uint NumberOfLinks;
    public uint FileIndexHigh;
    public uint FileIndexLow;
};

Note that in the above code I have included two lines like dirName1 = NormalizePath(dirName1); and have not specified what the function NormalizePath is. NormalizePath can be any path-normalization function - many have been provided in answers elsewhere. Providing a reasonable NormalizePath function means that AreDirsEqual will give a reasonable answer even when the two input paths refer to non-existent file system objects, i.e. to paths that you simply want to compare on a string-level.

(There may be subtle permissions issues with this code, if a user has only traversal permissions on some initial directories, I am not sure if the file system accesses required by AreFileSystemObjectsEqual are permitted. The parameter resolveJunctionaAndNetworkPaths at least allows the user to revert to pure textual comparison in this case...)

The idea for this came from a reply by Warren Stevens in a similar question I posted on SuperUser: https://superuser.com/a/881966/241981

Inclinable answered 30/8, 2022 at 19:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.