How to check if one path is a child of another path?

10,658

Solution 1

Unfortunately it's not as simple as StartsWith.

Here's a better answer, adapted from this duplicate question. I've made it an extension method for ease of use. Also using a brute-force catch as just about any method that accesses the file system can fail based on user permissions.

public static bool IsSubDirectoryOf(this string candidate, string other)
{
    var isChild = false;
    try
    {
        var candidateInfo = new DirectoryInfo(candidate);
        var otherInfo = new DirectoryInfo(other);

        while (candidateInfo.Parent != null)
        {
            if (candidateInfo.Parent.FullName == otherInfo.FullName)
            {
                isChild = true;
                break;
            }
            else candidateInfo = candidateInfo.Parent;
        }
    }
    catch (Exception error)
    {
        var message = String.Format("Unable to check directories {0} and {1}: {2}", candidate, other, error);
        Trace.WriteLine(message);
    }

    return isChild;
}

Solution 2

Any string-based solution is potentially subject to directory traversal attacks or correctness issues with things like trailing slashes. Unfortunately, the .NET Path class does not provide this functionality, however the Uri class does, in the form of Uri.IsBaseOf().

    Uri potentialBase = new Uri(@"c:\dir1\");

    Uri regular = new Uri(@"c:\dir1\dir2");

    Uri confusing = new Uri(@"c:\temp\..\dir1\dir2");

    Uri malicious = new Uri(@"c:\dir1\..\windows\system32\");

    Console.WriteLine(potentialBase.IsBaseOf(regular));   // True
    Console.WriteLine(potentialBase.IsBaseOf(confusing)); // True
    Console.WriteLine(potentialBase.IsBaseOf(malicious)); // False

Solution 3

I've used an extension method like this:

    /// <summary>
    /// Check if a directory is the base of another
    /// </summary>
    /// <param name="root">Candidate root</param>
    /// <param name="child">Child folder</param>
    public static bool IsBaseOf(this DirectoryInfo root, DirectoryInfo child)
    {
        var directoryPath = EndsWithSeparator(new Uri(child.FullName).AbsolutePath);
        var rootPath = EndsWithSeparator(new Uri(root.FullName).AbsolutePath);
        return directoryPath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase);
    }

    private static string EndsWithSeparator(string absolutePath)
    {
        return absolutePath?.TrimEnd('/','\\') + "/";
    }
Share:
10,658

Related videos on Youtube

user626528
Author by

user626528

Updated on June 04, 2022

Comments

  • user626528
    user626528 almost 2 years

    How to check if one path is a child of another path?
    Just checking for substring is not a way to go, because there can items such as . and .., etc

  • Mårten Wikström
    Mårten Wikström over 11 years
    But that won't work if on Windows and one path is lower case and the other upper case.
  • Captain Coder
    Captain Coder over 11 years
    Yup, its a pickle, the simple solution would be to ToUpperInvariant() the lot before comparing, but that should only be done on Windows. I've played around with GetFullPath() for a bit, and it does take elements from the input string into the output. It doesn't look at the filesystem to correctly case elements of the input string. So there is no way to do it platform independantly with GetFullPath()+ToUpperInvariant(). I'm not a fan of giving up compatibility, so I would suggest checking the platform in the Environment class and then use different checks to fix this issue.
  • Chad
    Chad almost 11 years
    I would recommend using the overload of "StartsWith" to ignore casing.
  • jpmc26
    jpmc26 about 10 years
    Does this work correctly if A is C:\my\dir and B is C:\my\dir2? That should be false, but I think Path.GetFullPath(B).StartsWith(Path.GetFullPath(A)) would be true.
  • pinkfloydx33
    pinkfloydx33 almost 10 years
    You could append ending slashes onto actual directory names. Then starts with would work if not case sensitive
  • jpmc26
    jpmc26 about 7 years
    IsBaseOf does not seem to work for this. Given the inputs 'C:\somerandomdir' and 'C:\someotherdir', I'm getting a true result.
  • techExplorer
    techExplorer about 7 years
    @Chad GetFullPath will return in same case for both so no use to ignore case here
  • Oren Melzer
    Oren Melzer almost 7 years
    @jpmc26, That's because you don't have a trailing slash. There is no way to know that 'somerandomdir' is a directory name and not a file name. If you want to handle this case, add a trailing slash prior to the call.
  • jpmc26
    jpmc26 almost 7 years
    Why does it matter? A file can't be the base of another file, anyway. Why does IsBaseOf even make such a weird guess as chopping off what it thinks is a file name when that clearly isn't the question the caller asked? If there's caveats and weird details like this to worry about, your answer should at least address them.
  • jpmc26
    jpmc26 over 6 years
    @jrh Only the user who asked the question can unaccept an answer.
  • Gene Pavlovsky
    Gene Pavlovsky about 5 years
    This is the safest and best solution
  • Herman
    Herman almost 5 years
    Swallowing the exception (without knowing how this method is used) seems like a bad practice. For example assume the method is used to prevent uploading information from a certain folder containing sensitive data, now when an exception happens the data will be uploaded.
  • Gerd K
    Gerd K about 4 years
    This also does not work for case sensitive paths. IsSubDirectoryOf(@"c:\a\b", @"c:\A") returns false
  • Hele
    Hele almost 4 years
    @jrh I believe @pinkfloydx33's suggestion fixes the issue in this answer. It would be great if the answer was edited to include it but .StartsWith seems to be a decent, simpler alternative to other answers posted here. More importantly, other answers don't provide any solid reason why .StartsWith won't work. The second highest voted answer by @Charlie doesn't address why .StartsWith doesn't work, it just declares it.
  • Hele
    Hele almost 4 years
    This answer does not address why the seemingly obvious solution .StartsWith doesn't work, rather it just states that and explains the more complex solution instead (-1)
  • jrh
    jrh almost 4 years
    @Hele jpmc26's comment covered one edge case, Does this work correctly if A is C:\my\dir and B is C:\my\dir2? That should be false, but I think Path.GetFullPath(B).StartsWith(Path.GetFullPath(A)) would be true. -- Another example, C:\abcd would be treated as a child of C:\ab, even though they have no relation to another. C:\abcd is not a subfolder of C:\ab, but StartsWith can't tell the difference, hope that explanation helps.
  • jrh
    jrh almost 4 years
    @Hele appending ending slashes would fix that specific C:\ab and C:\abcd problem but paths don't necessarily only have \\ and directory names in them, sometimes they have ".." and I don't remember if GetFullPath resolves those, so now another condition is this will only work on absolute paths, IMO string compare operations are not the way to go for handling paths. DirectoryInfo is what I would recommend.
  • jrh
    jrh almost 4 years
    Just checked, Path.GetFullPath does resolve "..", but I'm still not thrilled with the solution of manually concatenating slashes and there might've been another thing (it's been a while), it looks like Path.GetFullPath won't change the ending slash, so if you pass a path with an ending slash, the path it returns will have one, otherwise it won't. IMO the slash concatenation is harder to work with than DirectoryInfo.
  • Hele
    Hele almost 4 years
    @jrh As you found, GetFullPath handles "..". And the trailing slash issue is solved simply by doing $"{input.TrimEnd(Path.DirectorySeparatorChar)}{Path.Director‌​ySeparatorChar)}". I understand why you feel that DirectoryInfo is the "right" way of doing it, but it's much slower and often unnecessary. In addition, .StartsWith seems to be the intuitive solution and I don't think this answer deserves a negative score.
  • jrh
    jrh almost 4 years
    @Hele Since I unfortunately can't remember my original test cases, I have to admit that if this answer was edited to have the manual `\` concatenation, there isn't anything concrete that I can think of offhand that would go wrong. Without the edit, IMO this answer certainly deserves its downvotes as it's highly deceptive (it will appear to work, until it's presented with C:\abcd vs C:\ab). Personally I disagree that string concatenation is intuitive for paths in general; also how did you measure and find out that DirectoryInfo is much slower? I'm surprised that there's much overhead involved.
  • jrh
    jrh almost 4 years
    Note that this will not work if you attempt to compare paths of different casing and some other edge cases, unfortunately DirectoryInfo does not handle all of the quirks of Windows's file systems.
  • Hele
    Hele almost 4 years
    @jrh You're right, without the edit this answer still deserves a negative score. What I intended to convey in my previous comment is that the idea of using .StartsWith in itself does not deserve a negative score. I didn't measure the performance of DirectoryInfo, but it is expected to be much slower than a simple string compare since it involves disk accesses (especially if the disk is a network drive). Even if DirectoryInfo does not access disk at all (which is very unlikely), that's still an implementation detail and may change with future .NET versions.
  • user626528
    user626528 over 3 years
    Both the directories have to exist for this method to work, it's a serious limitation.
  • Peter Csala
    Peter Csala almost 3 years
    Welcome to StackOverflow. Please provide some explanation as well, not just code.
  • ErikE
    ErikE over 2 years
    This is problematic for one thing in that it assumes the file separator is `, when it could be .Net Core running on linux where the file separator is /. It also seems pointless to split, then compare each segment. Isn't that the same as a StartsWith`?