如何检查给定路径是否可能是另一个路径的子路径?

61

我正在尝试使用Java查找给定路径是否可能是另一个路径的子路径。两个路径都可能不存在。

比如c:\Program Files\My Company\test\My App可以是c:\Program Files的子路径。

目前我是这样做的:

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

这个例子需要文件系统IO吗? - user2586917
可能是Java:检查路径是否为文件的父级的重复问题。 - Suma
2
@Suma:你链接的问题是这个问题的_重复_。 - Jayan
10个回答

74
你还可以使用java.nio.file.Path更轻松地完成此操作。 java.nio.file.Path.startsWith方法似乎处理所有可能的情况。
示例:
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, "");
}

输出:

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

如果您需要更可靠的结果,可以使用 toRealPath 代替 toAbsolutePath

1
很棒的解决方案。只能在Java 7或更新版本中实现。 - Tim Bender
3
这个处理方式如何处理带有 .. 的路径? - Max
“toAbsolutePath” 方法解析路径中的“..”,因此它应该可以工作。不过最好进行测试。 - Jecho Jekov
17
为了正确处理 ..,我实际上需要使用 Paths.get(parentText).normalize()toAbsolutePath() 无法解决它们,而 toRealPath() 在文件不存在时会抛出 IOException 异常。这在使用 Java 7 的 OSX 和 Centos 6.7 上奏效。 - Matt D
这似乎只检查根目录下的目录,那么B文件夹呢?它不应该也是父级目录吗? - Calvin Taylor
在Windows上,C:\Program FilesC:\PROGRA~1\Foo 的祖先。同样地,C:\Documents and SettingsC:\Users\Public 的祖先。Path.startsWith() 无法处理这种情况,因此您必须在某个时候调用 Files.isSameFile()。我的答案试图处理这些边角情况。 - Bass

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

这似乎是最好的解决方案。它有什么漏洞吗? - Grumblesaurus
1
@JoshuaD 不是一个实际的漏洞,但在针对 possibleParent 本身进行测试时,它将返回 false。这可能是所期望的,也可能不是,具体取决于使用情况。(例如,在测试路径以查看它们是否包含在允许的目录树下时,这将在该树的根目录上失败。) - Ti Strga

11

除了文件路径可能不存在(以及规范化可能不成功)的事实外,这看起来是一个合理的方法,在简单情况下应该可以工作。

你可能想要查看在循环中调用getParentFile()的“maybe child”,并在每个步骤中测试它是否与父级匹配。如果父级不是(真实的)目录,还可以短路比较。

也许像以下代码一样:

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;
}

请注意,如果您希望子关系是严格的(即目录不是其自身的子目录),则可以更改第9行上的初始child赋值为child.getParentFile(),以便第一个检查发生在子目录的包含目录中。


2
尽管楼主没有明确说明,但很可能这个问题涉及到实际存在的文件而不是路径。+1 - biziclop

8
这将适用于您的示例。如果子路径是相对路径(这通常是可取的),它也会返回true
boolean myCheck(File maybeChild, File possibleParent)
{
    URI parentURI = possibleParent.toURI();
    URI childURI = maybeChild.toURI();
    return !parentURI.relativize(childURI).isAbsolute();
}

1
Spec 表示,_"如果 [给定的 URI 不是子级],则返回给定的 URI。"_ 这意味着最好将您的检查更改为 parentURI.relativize(childURI) != childURI。否则,如果 maybeChild 是绝对路径,则您的函数会给出错误的结果。 - SnakE
你说得对。我可能是想说,如果maybeChild是相对路径而不是possibleParent的子路径,你的方法仍然会返回true。但这实际上并不是问题,因为File.toURI()保证返回绝对URI,所以childURI始终是绝对的。尽管如此,我提出的检查也应该可以正常工作。 - SnakE
如果 maybeChild 是相对的,那么它可能是任何东西的子级 - 你无法确定。 - finnw
如果maybeChild是绝对路径,则它有可能来自另一台计算机,可能运行着另一个操作系统。你只需假设所涉及的路径是本地路径。同样,自然而然地假设相对路径是相对于Java虚拟机的当前工作目录。 - SnakE

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

4

按照现有的方式应该是可以正常工作的,不过我会使用getCanonicalPath()而不是getAbsolutePath()。这样可以规范化任何奇怪的路径,比如x/../y/z,否则可能会影响匹配。


12
不,不,这是不正确的!即使进行规范化,提问者的myCheck()方法会错误地指出 C:\ProgC:\Program Files 的子文件夹。请查看下面@biziclop的答案。 - Matt Quigley

1

在测试路径是否相等时,应考虑以下几点:

  1. 文件系统的大小写敏感性。唯一能处理大小写(不)敏感文件系统的API是NIO.2(1.7+),因此既不能使用java.io.File也不能使用String
  2. 单个路径条目处理:C:\abc既不是C:\abcd的祖先,也不是其直接父级,因此无法使用String.startsWith() API。
  3. Windows上,C:\Program FilesC:\PROGRA~1是同一个目录,Files.isSameFile()(来自NIO.2)是唯一能正确处理这种情况的API。这就是Path.startsWith()方法不支持的地方。
  4. 符号链接友好性(由于实际要求可能有所不同,因此未完全涵盖)。对于目录符号链接,Files.isSameFile()在某种程度上支持这一点,因此C:\Documents and Settings确实是C:\Users\Public的祖先。同样,这就是自定义代码比Path.startsWith() API稍微更好的地方(请参见this most-voted answer)。

说了以上内容,解决方案可能如下。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()
  }

1

令人惊讶的是,没有简单而功能强大的解决方案。

被接受的答案考虑了相同目录作为子目录,这是错误的。

这里提供一个只使用java.nio.file.Path API的解决方案:

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

测试用例:

 @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();
}

显然,文件确实位于其自己的子路径中。我会称相反的结果为违反直觉。 - Sergei Voitovich

1

注意相对路径!我认为最简单的解决方案是这样的:

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)
  }
}

在Scala中,您可以采用类似的方法:
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)

0
旧问题,但是1.7版本之前的解决方案:
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;
}

为了完整性,这是Java 1.7+的解决方案:
public boolean startsWith(String possibleRoot, String possibleChildOrSame) {
        Path p1 = Paths.get(possibleChildOrSame).toAbsolutePath();
        Path p2 = Paths.get(possibleRoot).toAbsolutePath();
        return p1.startsWith(p2);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接