开发者

How to check if a given path is possible child of another path?

I am trying to find if given path is possible child开发者_运维知识库 of another path using java. Both path may not exist.

Say c:\Program Files\My Company\test\My App is a possible child of c:\Program Files.

Currently I am doing this with

boolean myCheck(File maybeChild, File possibleParent)
{
    return maybeChild.getAbsolutePath().startsWith( possibleParent.getAbsolutePath());
}


You can also use java.nio.file.Path to do this much more easily. The java.nio.file.Path.startsWith method seems to handle all possible cases.

Example:

private static void isChild(Path child, String parentText) {
    Path parent = Paths.get(parentText).toAbsolutePath();
    System.out.println(parentText + " = " + child.startsWith(parent));
}

public static void main(String[] args) {
    Path child = Paths.get("/FolderA/FolderB/File").toAbsolutePath();
    isChild(child, "/FolderA/FolderB/File");
    isChild(child, "/FolderA/FolderB/F");
    isChild(child, "/FolderA/FolderB");
    isChild(child, "/FolderA/Folder");
    isChild(child, "/FolderA");
    isChild(child, "/Folder");
    isChild(child, "/");
    isChild(child, "");
}

Output:

/FolderA/FolderB/File = true
/FolderA/FolderB/F = false
/FolderA/FolderB = true
/FolderA/Folder = false
/FolderA = true
/Folder = false
/ = true
 = false

If you need more reliability you can use toRealPath instead of toAbsolutePath.


File parent = maybeChild.getParentFile();
while ( parent != null ) {
  if ( parent.equals( possibleParent ) )
    return true;
  parent = parent.getParentFile();
}
return false;


Asides from the fact the paths may not exist (and the canonicalisation may not succeed), this looks like a reasonable approach that should work in the straightforward case.

You may want to look at calling getParentFile() on the "maybe child" in a loop, testing if it matches the parent at each step. You can also short-circuit the comparison if the parent isn't a (real) directory.

Perhaps something like the following:

boolean myCheck(File maybeChild, File possibleParent) throws IOException
{
    final File parent = possibleParent.getCanonicalFile();
    if (!parent.exists() || !parent.isDirectory()) {
        // this cannot possibly be the parent
        return false;
    }

    File child = maybeChild.getCanonicalFile();
    while (child != null) {
        if (child.equals(parent)) {
            return true;
        }
        child = child.getParentFile();
    }
    // No match found, and we've hit the root directory
    return false;
}

Note that if you want the child relationship to be strict (i.e. a directory is not a child of itself) you can change the initial child assignment on line 9 to be child.getParentFile() so the first check happens on the child's containing directory.


This will work for your example. It will also return true if the child is a relative path (which is often desirable.)

boolean myCheck(File maybeChild, File possibleParent)
{
    URI parentURI = possibleParent.toURI();
    URI childURI = maybeChild.toURI();
    return !parentURI.relativize(childURI).isAbsolute();
}


That will probably work fine as it is, although I would use getCanonicalPath() rather than getAbsolutePath(). This should normalize any weird paths like x/../y/z which would otherwise screw up the matching.


maybeChild.getCanonicalPath().startsWith( possibleParent.getCanonicalPath() );


Be aware of relative paths! I think that simplest solution is something like this:

public boolean myCheck(File maybeChild, File possibleParent) {
  if (requestedFile.isAbsolute) {
    return possibleParent.resolve(maybeChild).normalize().toAbsolutePath.startsWith(possibleParent.normalize().toAbsolutePath)
  } else {
    return maybeChild.normalize().toAbsolutePath.startsWith(possibleParent.normalize().toAbsolutePath)
  }
}

In scala you can have similar approach:

val baseDir = Paths.get("/home/luvar/tmp")
val baseDirF = baseDir.toFile
//val requestedFile = Paths.get("file1")
val requestedFile = Paths.get("../.viminfo")
val fileToBeRead = if (requestedFile.isAbsolute) {
  requestedFile
} else {
  baseDir.resolve(requestedFile)
}
fileToBeRead.toAbsolutePath
baseDir.toAbsolutePath
fileToBeRead.normalize()
baseDir.normalize()
val isSubpath = fileToBeRead.normalize().toAbsolutePath.startsWith(baseDir.normalize().toAbsolutePath)


Old question but a pre-1.7 solution:

public boolean startsWith(String possibleRoot, String possibleChildOrSame) {
        String[] possiblePath = new File(possibleRoot).getAbsolutePath().replace('\\', '/').split("/");
        String[] possibleChildOrSamePath = new File(possibleChildOrSame).getAbsolutePath().replace('\\', '/').split("/");

        if (possibleChildOrSamePath.length < possiblePath.length) {
            return false;
        }

        // not ignoring case
        for (int i = 0; i < possiblePath.length; i++) {
            if (!possiblePath[i].equals(possibleChildOrSamePath[i])) {
                return false;
            }
        }
        return true;
}

For completeness the java 1.7+ solution:

public boolean startsWith(String possibleRoot, String possibleChildOrSame) {
        Path p1 = Paths.get(possibleChildOrSame).toAbsolutePath();
        Path p2 = Paths.get(possibleRoot).toAbsolutePath();
        return p1.startsWith(p2);
}


Surprisingly there is no simple, yet functional solution.

The accepted answer does consider same directories as child, which is wrong.

Here is one using java.nio.file.Path API only:

static boolean isChildPath(Path parent, Path child){
      Path pn = parent.normalize();
      Path cn = child.normalize();
      return cn.getNameCount() > pn.getNameCount() && cn.startsWith(pn);
}

Test cases:

 @Test
public void testChildPath() {
      assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F"))).isFalse();
      assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/A"))).isTrue();
      assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/A.txt"))).isTrue();

      assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/../A"))).isFalse();
      assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/FA"))).isFalse();

      assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderA"))).isFalse();
      assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderA/B"))).isTrue();
      assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderA/B"))).isTrue();
      assertThat(isChildPath(Paths.get("FolderA"), Paths.get("FolderAB"))).isFalse();
      assertThat(isChildPath(Paths.get("/FolderA/FolderB/F"), Paths.get("/FolderA/FolderB/F/Z/X/../A"))).isTrue();
}


When testing paths for equality, the following considerations should be taken into account:

  1. Case sensitivity of the file system. The only API which can handle case (in)sensitive file systems is NIO.2 (1.7+), that's why neither java.io.File nor String can be used.
  2. Individual path entry handling: C:\abc is not an ancestor, nor even an immediate parent of C:\abcd, hence String.startsWith() API can't be used.
  3. On Windows, C:\Program Files is the same directory as C:\PROGRA~1, and Files.isSameFile() (from NIO.2) is the only API that can handle this right. This is what Path.startsWith() approach doesn't support.
  4. Symlink friendliness (not fully covered by my answer since the actual requirements may differ). For directory symlinks, Files.isSameFile() supports this to some extent, so that C:\Documents and Settings is indeed an ancestor of C:\Users\Public. Again, this is where custom code works slightly better than the Path.startsWith() API (see this most-voted answer).

Having said the above, the solution may look like this. Java:

  boolean isAncestorOf(final Path parent, final Path child) {
    final Path absoluteParent = parent.toAbsolutePath().normalize();
    final Path absoluteChild = child.toAbsolutePath().normalize();

    if (absoluteParent.getNameCount() >= absoluteChild.getNameCount()) {
      return false;
    }

    final Path immediateParent = absoluteChild.getParent();
    if (immediateParent == null) {
      return false;
    }

    return isSameFileAs(absoluteParent, immediateParent) || isAncestorOf(absoluteParent, immediateParent);
  }

  boolean isSameFileAs(final Path path, final Path path2) {
    try {
      return Files.isSameFile(path, path2);
    }
    catch (final IOException ioe) {
      return path.toAbsolutePath().normalize().equals(path2.toAbsolutePath().normalize());
    }
  }

Kotlin:

fun Path.isAncestorOf(child: Path): Boolean {
  val absoluteParent = toAbsolutePath().normalize()
  val absoluteChild = child.toAbsolutePath().normalize()

  if (absoluteParent.nameCount >= absoluteChild.nameCount) {
    return false
  }

  val immediateParent = absoluteChild.parent
                        ?: return false

  return absoluteParent.isSameFileAs(immediateParent) || absoluteParent.isAncestorOf(immediateParent)
}

fun Path.isSameFileAs(that: Path): Boolean =
  try {
    Files.isSameFile(this, that)
  }
  catch (_: NoSuchFileException) {
    toAbsolutePath().normalize() == that.toAbsolutePath().normalize()
  }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜