Given full path, check if path is subdirectory of some other path, or otherwise

24,214

Solution 1

DirectoryInfo di1 = new DirectoryInfo(dir1);
DirectoryInfo di2 = new DirectoryInfo(dir2);
bool isParent = di2.Parent.FullName == di1.FullName;

Or in a loop to allow for nested sub-directories, i.e. C:\foo\bar\baz is a sub directory of C:\foo :

DirectoryInfo di1 = new DirectoryInfo(dir1);
DirectoryInfo di2 = new DirectoryInfo(dir2);
bool isParent = false;
while (di2.Parent != null)
{
    if (di2.Parent.FullName == di1.FullName)
    {
        isParent = true;
        break;
    }
    else di2 = di2.Parent;
}

Solution 2

  • Case insensitive
  • Tolerates mix of \ and / folder delimiters
  • Tolerates ..\ in path
  • Avoids matching on partial folder names (c:\foobar not a subpath of c:\foo)

Note: This only matches on the path string and does not work for symbolic links and other kinds of links in the filesystem.

Code:

public static class StringExtensions
{
    /// <summary>
    /// Returns true if <paramref name="path"/> starts with the path <paramref name="baseDirPath"/>.
    /// The comparison is case-insensitive, handles / and \ slashes as folder separators and
    /// only matches if the base dir folder name is matched exactly ("c:\foobar\file.txt" is not a sub path of "c:\foo").
    /// </summary>
    public static bool IsSubPathOf(this string path, string baseDirPath)
    {
        string normalizedPath = Path.GetFullPath(path.Replace('/', '\\')
            .WithEnding("\\"));

        string normalizedBaseDirPath = Path.GetFullPath(baseDirPath.Replace('/', '\\')
            .WithEnding("\\"));

        return normalizedPath.StartsWith(normalizedBaseDirPath, StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Returns <paramref name="str"/> with the minimal concatenation of <paramref name="ending"/> (starting from end) that
    /// results in satisfying .EndsWith(ending).
    /// </summary>
    /// <example>"hel".WithEnding("llo") returns "hello", which is the result of "hel" + "lo".</example>
    public static string WithEnding([CanBeNull] this string str, string ending)
    {
        if (str == null)
            return ending;

        string result = str;

        // Right() is 1-indexed, so include these cases
        // * Append no characters
        // * Append up to N characters, where N is ending length
        for (int i = 0; i <= ending.Length; i++)
        {
            string tmp = result + ending.Right(i);
            if (tmp.EndsWith(ending))
                return tmp;
        }

        return result;
    }

    /// <summary>Gets the rightmost <paramref name="length" /> characters from a string.</summary>
    /// <param name="value">The string to retrieve the substring from.</param>
    /// <param name="length">The number of characters to retrieve.</param>
    /// <returns>The substring.</returns>
    public static string Right([NotNull] this string value, int length)
    {
        if (value == null)
        {
            throw new ArgumentNullException("value");
        }
        if (length < 0)
        {
            throw new ArgumentOutOfRangeException("length", length, "Length is less than zero");
        }

        return (length < value.Length) ? value.Substring(value.Length - length) : value;
    }
}

Test cases (NUnit):

[TestFixture]
public class StringExtensionsTest
{
    [TestCase(@"c:\foo", @"c:", Result = true)]
    [TestCase(@"c:\foo", @"c:\", Result = true)]
    [TestCase(@"c:\foo", @"c:\foo", Result = true)]
    [TestCase(@"c:\foo", @"c:\foo\", Result = true)]
    [TestCase(@"c:\foo\", @"c:\foo", Result = true)]
    [TestCase(@"c:\foo\bar\", @"c:\foo\", Result = true)]
    [TestCase(@"c:\foo\bar", @"c:\foo\", Result = true)]
    [TestCase(@"c:\foo\a.txt", @"c:\foo", Result = true)]
    [TestCase(@"c:\FOO\a.txt", @"c:\foo", Result = true)]
    [TestCase(@"c:/foo/a.txt", @"c:\foo", Result = true)]
    [TestCase(@"c:\foobar", @"c:\foo", Result = false)]
    [TestCase(@"c:\foobar\a.txt", @"c:\foo", Result = false)]
    [TestCase(@"c:\foobar\a.txt", @"c:\foo\", Result = false)]
    [TestCase(@"c:\foo\a.txt", @"c:\foobar", Result = false)]
    [TestCase(@"c:\foo\a.txt", @"c:\foobar\", Result = false)]
    [TestCase(@"c:\foo\..\bar\baz", @"c:\foo", Result = false)]
    [TestCase(@"c:\foo\..\bar\baz", @"c:\bar", Result = true)]
    [TestCase(@"c:\foo\..\bar\baz", @"c:\barr", Result = false)]
    public bool IsSubPathOfTest(string path, string baseDirPath)
    {
        return path.IsSubPathOf(baseDirPath);
    }
}

Updates

  • 2015-08-18: Fix bug matching on partial folder names. Add test cases.
  • 2015-09-02: Support ..\ in paths, add missing code
  • 2017-09-06: Add note on symbolic links.

Solution 3

Try:

dir1.contains(dir2+"\\");

Solution 4

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

var relPath = Path.GetRelativePath(basePath, subPath);
var isSubPath = !relPath.StartsWith('.') && !Path.IsPathRooted(relPath);

Both subPath and basePath must be absolut paths.

Convenience extension function:

public static bool IsSubPathOf(this string subPath, string basePath) {
    var rel = Path.GetRelativePath(basePath, subPath);
    return !rel.StartsWith('.') && !Path.IsPathRooted(rel);
}

.NET Fiddle with some test cases: https://dotnetfiddle.net/9FIU0g

Solution 5

string path1 = "C:\test";
string path2 = "C:\test\abc";

var root = Path.GetFullPath(path1);
var secondDir = Path.GetFullPath(path2 + Path.AltDirectorySeparatorChar);

if (!secondDir.StartsWith(root))
{
}

Path.GetFullPath works great with paths, like: C:\test\..\forbidden\

Share:
24,214
andree
Author by

andree

Updated on March 31, 2021

Comments

  • andree
    andree about 3 years

    I have 2 strings - dir1 and dir2, and I need to check if one is sub-directory for other. I tried to go with Contains method:

    dir1.contains(dir2);
    

    but that also returns true, if directories have similar names, for example - c:\abc and c:\abc1 are not sub-directories, bet returns true. There must be a better way.

  • Darcara
    Darcara about 11 years
    This works only if the directories lack the final slash. See Why isn't this DirectoryInfo comparison working?
  • milanio
    milanio over 8 years
    What about path C:\foo\bar\..\bar2 vs C:\foo\bar2? Or C:\foo\bar\ vs C:\foo\bar\..\..\?
  • angularsen
    angularsen over 8 years
    Good point. I believe we should add Path.GetFullPath() to resolve those examples.
  • angularsen
    angularsen over 8 years
    Added three more test cases and fixed the implementation to support your examples. Also added two missing extension methods the implementation relied on. I'm sure this can all be simplified, but it seems to work.
  • Marcus Mangelsdorf
    Marcus Mangelsdorf over 8 years
    This unfortunately returns true for rootPath = @"c:\foo" and subPath = @"c:\foobar" - which is obviously a false positive.
  • emery.noel
    emery.noel almost 7 years
    For me, Path.GetFullPath("c:") returns "c:\windows\system32"
  • angularsen
    angularsen almost 7 years
    @emery.noel I believe that is expected behavior, meaning you only specify a drive and not a path. Path.GetFullPath(@"c:\") returns c:\ to me. How does this relate to the implementation of IsSubpathOf()?
  • emery.noel
    emery.noel almost 7 years
    @anjdreas it is your first test case. I did not see how it could pass. For me, I had to append slashes to directories BEFORE I call GetFullPath or I get unexpected results.
  • emery.noel
    emery.noel almost 7 years
    @anjdreas ... and of course now I go back and look, I see that you are doing it too. Missed the 2nd parenthesis.
  • Kevin Shea
    Kevin Shea over 6 years
    Unfortunately doesn't work when symbolic links are involved (and presumably directory junctions). Still, +1 for covering the vast majority of directory layouts.
  • angularsen
    angularsen over 6 years
    @KevinShea Yes, this only works on path strings, not on the filesystem level. I added a note to the answer now. To my knowledge, Windows API does not have a way to test for similar paths across symbolic links, network shares etc. You could do a check of created and edited timestamps, then finally a full checksum of the files, but that may not be a feasible solution in many cases.
  • STLDev
    STLDev over 5 years
    It's worth noting that the [CanBeNull] and [NotNull] annotations are part of the JetBrains.Annotations nuget package. Find them here: JetBrains.Annotations.
  • Josef Bláha
    Josef Bláha over 4 years
    This code ignores case (in)sensitivity of the platform. Otherwise it seems simple and working!
  • Konstantin
    Konstantin over 4 years
    Imagine 2 directories: C:\SomeDirectory and C:\SomeDirectoryBackup this will give true, even though the second directory is not a child of the first one
  • papplesharp
    papplesharp about 3 years
    On windows it is case-insensitive and this will fail