From 6db30809b4ca5d9a79c67ef75c6b636bfd3ca0d8 Mon Sep 17 00:00:00 2001 From: Anton Makeev Date: Thu, 2 Jul 2015 13:16:19 +0200 Subject: [PATCH] FileUtil.toCanonicalPath can now expand symlinks when it's required to build the valid path that contains /../ --- .../intellij/openapi/util/io/FileUtil.java | 62 +++++++++++++++++-- .../openapi/util/io/FileUtilHeavyTest.java | 53 ++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/platform/util/src/com/intellij/openapi/util/io/FileUtil.java b/platform/util/src/com/intellij/openapi/util/io/FileUtil.java index 7ad8343fc52d..e48f1b4d995f 100644 --- a/platform/util/src/com/intellij/openapi/util/io/FileUtil.java +++ b/platform/util/src/com/intellij/openapi/util/io/FileUtil.java @@ -645,13 +645,35 @@ public class FileUtil extends FileUtilRt { /** * Converts given path to canonical representation by eliminating '.'s, traversing '..'s, and omitting duplicate separators. * Please note that this method is symlink-unfriendly (i.e. result of "/path/to/link/../next" most probably will differ from - * what {@link java.io.File#getCanonicalPath()} will return) - so use with care. + * what {@link java.io.File#getCanonicalPath()} will return) - so use with care.
+ *
+ * If the path may contain synlinks, use use {@link FileUtil#toCanonicalPath(String, boolean)} */ @Contract("null -> null") public static String toCanonicalPath(@Nullable String path) { return toCanonicalPath(path, File.separatorChar, true); } + /** + * When relative ../ parts do not escape outside of symlinks, the links are not expanded.
+ * That is, in the best-case scenario the original non-expanded path is preserved.
+ *
+ * Otherwise, returns a fully resolved path using {@link java.io.File#getCanonicalPath()}.
+ *
+ * Consider the following case: + *
+   * root/
+   *   dir1/
+   *     link_to_dir1
+   *   dir2/
+   * 
+ * 'root/dir1/link_to_dir1/../dir2' should be resolved to 'root/dir2' + */ + @Contract("null, _ -> null") + public static String toCanonicalPath(@Nullable String path, boolean resolveSymlinksIfNecessary) { + return toCanonicalPath(path, File.separatorChar, true, resolveSymlinksIfNecessary); + } + @Contract("null, _ -> null") public static String toCanonicalPath(@Nullable String path, char separatorChar) { return toCanonicalPath(path, separatorChar, true); @@ -664,6 +686,14 @@ public class FileUtil extends FileUtilRt { @Contract("null, _, _ -> null") private static String toCanonicalPath(@Nullable String path, char separatorChar, boolean removeLastSlash) { + return toCanonicalPath(path, separatorChar, removeLastSlash, false); + } + + @Contract("null, _, _, _ -> null") + private static String toCanonicalPath(@Nullable String path, + final char separatorChar, + final boolean removeLastSlash, + final boolean resolveSymlinksIfNecessary) { if (path == null || path.isEmpty()) { return path; } @@ -676,6 +706,21 @@ public class FileUtil extends FileUtilRt { return path; } + final String finalPath = path; + NotNullProducer realCanonicalPath = !resolveSymlinksIfNecessary ? null : new NotNullProducer() { + @NotNull + @Override + public String produce() { + try { + return new File(finalPath).getCanonicalPath().replace(separatorChar, '/'); + } + catch (IOException ignore) { + // fall back to the default behavior + return toCanonicalPath(finalPath, separatorChar, removeLastSlash, false); + } + } + }; + StringBuilder result = new StringBuilder(path.length()); int start = processRoot(path, result); int dots = 0; @@ -685,7 +730,9 @@ public class FileUtil extends FileUtilRt { char c = path.charAt(i); if (c == '/') { if (!separator) { - processDots(result, dots, start); + if (!processDots(result, dots, start, resolveSymlinksIfNecessary)) { + return realCanonicalPath.produce(); + } dots = 0; } separator = true; @@ -710,7 +757,9 @@ public class FileUtil extends FileUtilRt { } if (dots > 0) { - processDots(result, dots, start); + if (!processDots(result, dots, start, resolveSymlinksIfNecessary)) { + return realCanonicalPath.produce(); + } } int lastChar = result.length() - 1; @@ -754,7 +803,8 @@ public class FileUtil extends FileUtilRt { return 0; } - private static void processDots(@NotNull StringBuilder result, int dots, int start) { + @Contract("_, _, _, false -> true") + private static boolean processDots(@NotNull StringBuilder result, int dots, int start, boolean resolveSymlinksIfNecessary) { if (dots == 2) { int pos = -1; if (!StringUtil.endsWith(result, "/../") && !StringUtil.equals(result, "../")) { @@ -770,6 +820,9 @@ public class FileUtil extends FileUtilRt { } } if (pos >= 0) { + if (resolveSymlinksIfNecessary && FileSystemUtil.isSymLink(new File(result.toString()))) { + return false; + } result.delete(pos, result.length()); } else { @@ -780,6 +833,7 @@ public class FileUtil extends FileUtilRt { StringUtil.repeatSymbol(result, '.', dots); result.append('/'); } + return true; } /** diff --git a/platform/util/testSrc/com/intellij/openapi/util/io/FileUtilHeavyTest.java b/platform/util/testSrc/com/intellij/openapi/util/io/FileUtilHeavyTest.java index bd6c32bfb57e..0e118cab81bb 100644 --- a/platform/util/testSrc/com/intellij/openapi/util/io/FileUtilHeavyTest.java +++ b/platform/util/testSrc/com/intellij/openapi/util/io/FileUtilHeavyTest.java @@ -227,6 +227,59 @@ public class FileUtilHeavyTest { FileUtil.delete(linkDir); assertEquals(1, targetDir.list().length); } + + + @Test + public void testToCanonicalPathSymLinksAware() throws Exception { + assumeTrue(SystemInfo.areSymLinksSupported); + + File root = IoTestUtil.createTestDir(myTempDirectory, "root"); + assertTrue(new File(root, "dir1/dir2/dir3/dir4").mkdirs()); + + // non-recursive link + IoTestUtil.createSymLink(new File(root, "dir1/dir2").getPath(), new File(root, "dir1/dir2_link").getPath()); + // recursive links to a parent dir + IoTestUtil.createSymLink(new File(root, "dir1").getPath(), new File(root, "dir1/dir1_link").getPath()); + + // I) links should NOT be resolved when ../ stays inside the linked path + // I.I) non-recursive links + assertEquals(root + "/dir1/dir2_link", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/./", true)); + assertEquals(root + "/dir1/dir2_link", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/dir3/../", true)); + assertEquals(root + "/dir1/dir2_link/dir3", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/dir3/dir4/../", true)); + assertEquals(root + "/dir1/dir2_link", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/dir3/dir4/../../", true)); + assertEquals(root + "/dir1/dir2_link", FileUtil.toCanonicalPath(root + "/dir1/../dir1/dir2_link/dir3/../", true)); + + // I.II) recursive links + assertEquals(root + "/dir1/dir1_link", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/./", true)); + assertEquals(root + "/dir1/dir1_link", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/dir2/../", true)); + assertEquals(root + "/dir1/dir1_link/dir2", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/dir2/dir3/../", true)); + assertEquals(root + "/dir1/dir1_link", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/dir2/dir3/../../", true)); + assertEquals(root + "/dir1/dir1_link", FileUtil.toCanonicalPath(root + "/dir1/../dir1/dir1_link/dir2/../", true)); + + // II) links should be resolved is ../ escapes outside + + // II.I) non-recursive links + assertEquals(root + "/dir1", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/../", true)); + assertEquals(root + "/dir1/dir2", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/../dir2", true)); + assertEquals(root + "/dir1/dir2", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/../../dir1/dir2", true)); + assertEquals(root + "/dir1/dir2", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/dir3/../../dir2", true)); + assertEquals(root + "/dir1/dir2", FileUtil.toCanonicalPath(root + "/dir1/dir2_link/dir3/../../../dir1/dir2", true)); + assertEquals(root + "/dir1/dir2", FileUtil.toCanonicalPath(root + "/dir1/../dir1/dir2_link/../dir2", true)); + + // II.I) recursive links + assertEquals(root.getPath(), FileUtil.toCanonicalPath(root + "/dir1/dir1_link/../", true)); + assertEquals(root + "/dir1", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/../dir1", true)); + assertEquals(root + "/dir1", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/../../root/dir1", true)); + assertEquals(root + "/dir1", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/dir3/../../dir1", true)); + assertEquals(root + "/dir1", FileUtil.toCanonicalPath(root + "/dir1/dir1_link/dir3/../../../root/dir1", true)); + assertEquals(root + "/dir1", FileUtil.toCanonicalPath(root + "/dir1/../dir1/dir1_link/../dir1", true)); + + // some corner cases, behavior should be the same as the default FileUtil.toCanonicalPath + assertEquals(FileUtil.toCanonicalPath("..", false), FileUtil.toCanonicalPath("..", true)); + assertEquals(FileUtil.toCanonicalPath("../", false), FileUtil.toCanonicalPath("../", true)); + assertEquals(FileUtil.toCanonicalPath("/..", false), FileUtil.toCanonicalPath("/..", true)); + assertEquals(FileUtil.toCanonicalPath("/../", false), FileUtil.toCanonicalPath("/../", true)); + } @Test public void testCaseSensitivityDetection() throws IOException {