mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
FileUtil.toCanonicalPath can now expand symlinks when it's required to build the valid path that contains /../
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user