FileUtil.toCanonicalPath can now expand symlinks when it's required to build the valid path that contains /../

This commit is contained in:
Anton Makeev
2015-07-02 13:50:46 +02:00
parent ddb47d0261
commit 6db30809b4
2 changed files with 111 additions and 4 deletions
@@ -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.<br>
* <br>
* 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.<br>
* That is, in the best-case scenario the original non-expanded path is preserved.<br>
* <br>
* Otherwise, returns a fully resolved path using {@link java.io.File#getCanonicalPath()}.<br>
* <br>
* Consider the following case:
* <pre>
* root/
* dir1/
* link_to_dir1
* dir2/
* </pre>
* '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<String> realCanonicalPath = !resolveSymlinksIfNecessary ? null : new NotNullProducer<String>() {
@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;
}
/**
@@ -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 {