Skip to content

Commit a20bea2

Browse files
committed
UnifiedTree.isRecursiveLink() links check may silently ignore some links
- Main fix: the child path created from the link should use `realParentPath` to avoid silent `NoSuchFileException`'s if the constructed link does not match actual (fully resolved) file system state of the local file we are checking. Original code run into (silent) exceptions and because of that was not able to detect some of possible recursive links. - Removed pattern checking for "../X" because backwards links can also be created with absolute paths, as seen in `Bug_185247_recursiveLinks.test5_linkParentDirectoyTwiceWithAbsolutePath(boolean)` - Added regression test `SymlinkResourceTest.testGithubBug2220`. - Added `UnifiedTree` methods to set/read `disable_advanced_recursive_link_checks` flag in tests so we can test both variants of link checking code. - Updated existing tests to test both advanced / simple link checks (where possible): `SymlinkResourceTest`, `Bug_185247_recursiveLinks` & `Bug_185247_LinuxTests` - Improved `WorkspaceResetExtension` to properly report test names (it only reported `false` or `true` for parametrized tests). Fixes #2220
1 parent f394888 commit a20bea2

File tree

5 files changed

+271
-110
lines changed

5 files changed

+271
-110
lines changed

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTree.java

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,6 @@ private static class PatternHolder {
438438
//Pattern: A UNIX or Windows relative path that just points backward
439439
static final Pattern TRIVIAL_SYMLINK_PATTERN = Pattern.compile( //
440440
Platform.OS.isWindows() ? "\\.[.\\\\]*" : "\\.[./]*"); //$NON-NLS-1$//$NON-NLS-2$
441-
442-
static final Pattern REPEATING_BACKWARDS_PATTERN = Pattern.compile( //
443-
Platform.OS.isWindows() ? "(\\.\\.\\\\)+.*" : "(\\.\\./)+.*"); //$NON-NLS-1$//$NON-NLS-2$
444441
}
445442

446443
/**
@@ -522,20 +519,10 @@ private boolean isRecursiveLink(IFileStore parentStore, IFileInfo localInfo) {
522519
Path realParentPath = parent.toRealPath();
523520
if (disable_advanced_recursive_link_checks) {
524521
// Multiple ../ backwards links can go outside the project tree
525-
if (linkTarget != null && PatternHolder.REPEATING_BACKWARDS_PATTERN.matcher(linkTarget).matches()) {
526-
Path targetPath = parent.resolve(linkTarget).normalize();
527-
528-
// Recursive if literal target points to the literal parent of this tree
529-
if (parent.normalize().startsWith(targetPath)) {
522+
if (linkTarget != null) {
523+
if (isRecursiveBackwardsLink(realParentPath, linkTarget)) {
530524
return true;
531525
}
532-
533-
// Recursive if resolved target points to the resolved parent of this tree
534-
Path realTargetPath = targetPath.toRealPath();
535-
if (realParentPath.startsWith(realTargetPath)) {
536-
return true;
537-
}
538-
539526
// If link is outside the project tree, consider as non recursive
540527
// The link still can create recursion in the tree, but we can't detect it here.
541528
}
@@ -573,6 +560,51 @@ private boolean isRecursiveLink(IFileStore parentStore, IFileInfo localInfo) {
573560
return false;
574561
}
575562

563+
/**
564+
* @param realParentPath real parent path object obtained as a result
565+
* of @code{Path.toRealPath()}
566+
* @param linkTarget the link target path as a string, may be relative or
567+
* absolute
568+
* @return true if the given target points backwards recursively to the given
569+
* parent path
570+
* @throws IOException
571+
*/
572+
private static boolean isRecursiveBackwardsLink(Path realParentPath, String linkTarget)
573+
throws IOException {
574+
// Cheap test first: literal target points to the literal parent
575+
Path normalizedLink = realParentPath.resolve(linkTarget).normalize();
576+
if (realParentPath.startsWith(normalizedLink)) {
577+
return true;
578+
}
579+
// Next check costs more time because it does real IO when resolving paths
580+
Path realTarget = normalizedLink.toRealPath();
581+
if (realParentPath.startsWith(realTarget)) {
582+
return true;
583+
}
584+
return false;
585+
}
586+
587+
/**
588+
* Disable or enable advanced recursive link checks. Advanced link checks may
589+
* hide valid directories in some cases, see bug 537449.
590+
*
591+
* @param enable <code>true</code> to enable advanced recursive link checks,
592+
* <code>false</code> to disable them.
593+
*/
594+
public static void enableAdvancedRecursiveLinkChecks(boolean enable) {
595+
disable_advanced_recursive_link_checks = !enable;
596+
}
597+
598+
/**
599+
* Returns whether advanced recursive link checks are enabled.
600+
*
601+
* @return <code>true</code> if advanced recursive link checks are enabled,
602+
* <code>false</code> otherwise.
603+
*/
604+
public static boolean isAdvancedRecursiveLinkChecksEnabled() {
605+
return !disable_advanced_recursive_link_checks;
606+
}
607+
576608
protected boolean isValidLevel(int currentLevel, int depth) {
577609
return switch (depth) {
578610
case IResource.DEPTH_INFINITE -> true;

resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/SymlinkResourceTest.java

Lines changed: 134 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,33 @@
2121
import static org.eclipse.core.tests.resources.ResourceTestUtil.createInWorkspace;
2222
import static org.eclipse.core.tests.resources.ResourceTestUtil.createTestMonitor;
2323
import static org.eclipse.core.tests.resources.ResourceTestUtil.waitForRefresh;
24-
import static org.junit.Assert.assertEquals;
25-
import static org.junit.Assert.assertTrue;
26-
import static org.junit.Assume.assumeTrue;
24+
import static org.junit.jupiter.api.Assertions.assertEquals;
25+
import static org.junit.jupiter.api.Assertions.assertTrue;
26+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
2727

2828
import java.io.IOException;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
31+
import java.nio.file.Paths;
2932
import org.eclipse.core.filesystem.EFS;
3033
import org.eclipse.core.filesystem.IFileStore;
34+
import org.eclipse.core.internal.localstore.UnifiedTree;
3135
import org.eclipse.core.resources.IProject;
3236
import org.eclipse.core.resources.IResource;
3337
import org.eclipse.core.resources.IResourceVisitor;
3438
import org.eclipse.core.resources.IWorkspaceRunnable;
3539
import org.eclipse.core.runtime.CoreException;
3640
import org.eclipse.core.runtime.IPath;
37-
import org.eclipse.core.tests.resources.WorkspaceTestRule;
38-
import org.junit.Rule;
39-
import org.junit.Test;
40-
41+
import org.eclipse.core.runtime.Platform.OS;
42+
import org.eclipse.core.tests.resources.util.WorkspaceResetExtension;
43+
import org.junit.jupiter.api.Test;
44+
import org.junit.jupiter.api.extension.ExtendWith;
45+
import org.junit.jupiter.params.ParameterizedTest;
46+
import org.junit.jupiter.params.provider.ValueSource;
47+
48+
@ExtendWith(WorkspaceResetExtension.class)
4149
public class SymlinkResourceTest {
4250

43-
@Rule
44-
public WorkspaceTestRule workspaceRule = new WorkspaceTestRule();
45-
4651
private void mkLink(IFileStore dir, String src, String tgt, boolean isDir) throws CoreException, IOException {
4752
createSymLink(dir.toLocalFile(EFS.NONE, createTestMonitor()), src, tgt, isDir);
4853
}
@@ -70,6 +75,88 @@ protected void createBug358830Structure(IFileStore rootDir) throws CoreException
7075
mkLink(folderA, "link", IPath.fromOSString("../").toOSString(), true);
7176
}
7277

78+
/**
79+
* Test a case of both recursive and non recursive symbolic links that uncovered
80+
* issue described in <a href=
81+
* "https://github.com/eclipse-platform/eclipse.platform/issues/2220">GitHub bug
82+
* 2220</a>.
83+
*
84+
* <pre>{@code
85+
* /A/B -> /X/Y/Z (B is symbolic link as precondition)
86+
* /A/B/C/D -> ../../D (non-recursive)
87+
* /A/B/C/E -> ../../Z (recursive)
88+
* }</pre>
89+
*
90+
* The starting path /A/B/C is already based on link. The real path of start is
91+
* /X/Y/Z/C so ../../Z points (recursive) to /X/Y/Z. /X/Y/D and /X/Y/Z is what
92+
* we expect to resolve but we fail in both cases with NoSuchFileException.
93+
*/
94+
@ParameterizedTest
95+
@ValueSource(booleans = { false, true })
96+
public void testGithubBug2220(boolean useAdvancedLinkCheck) throws Exception {
97+
assumeTrue(canCreateSymLinks(), "only relevant for platforms supporting symbolic links");
98+
assumeTrue(!OS.isWindows(), "Windows file system handles recursive links differently");
99+
final boolean originalValue = UnifiedTree.isAdvancedRecursiveLinkChecksEnabled();
100+
try {
101+
UnifiedTree.enableAdvancedRecursiveLinkChecks(useAdvancedLinkCheck);
102+
IProject project = getWorkspace().getRoot().getProject("testGithubBug2220");
103+
createInWorkspace(project);
104+
105+
/* Re-use projects which are cleaned up automatically */
106+
getWorkspace().run((IWorkspaceRunnable) monitor -> {
107+
/* delete open project because we must re-open with BACKGROUND_REFRESH */
108+
project.delete(IResource.NEVER_DELETE_PROJECT_CONTENT, createTestMonitor());
109+
project.create(null);
110+
try {
111+
createGithubBug2220Structure(EFS.getStore(project.getLocationURI()));
112+
} catch (IOException e) {
113+
throw new IllegalStateException("unexpected IOException occurred", e);
114+
}
115+
// Bug only happens with BACKGROUND_REFRESH.
116+
project.open(IResource.BACKGROUND_REFRESH, createTestMonitor());
117+
}, null);
118+
119+
// wait for BACKGROUND_REFRESH to complete.
120+
waitForRefresh();
121+
project.accept(new IResourceVisitor() {
122+
int resourceCount = 0;
123+
124+
@Override
125+
public boolean visit(IResource resource) {
126+
resourceCount++;
127+
// We have 1 root + .settings + prefs + + .project + 10 elements --> 14 elements
128+
// to visit at most
129+
System.out.println(resourceCount + " visited: " + resource.getFullPath());
130+
assertTrue(resourceCount <= 15, "Expected max 15 elements to visit, got: " + resourceCount);
131+
return true;
132+
}
133+
});
134+
} finally {
135+
UnifiedTree.enableAdvancedRecursiveLinkChecks(originalValue);
136+
}
137+
}
138+
139+
/**
140+
* <pre>{@code
141+
* /A/B -> /X/Y/Z (B is symbolic link as precondition)
142+
* /A/B/C/D -> ../../D (non-recursive)
143+
* /A/B/C/E -> ../../Z (recursive)
144+
* }</pre>
145+
*
146+
* The starting path /A/B/C is already based on link. The real path of start is
147+
* /X/Y/Z/C so ../../Z points (recursive) to /X/Y/Z.
148+
*/
149+
protected void createGithubBug2220Structure(IFileStore rootDir) throws CoreException, IOException {
150+
Path root = rootDir.toLocalFile(EFS.NONE, createTestMonitor()).toPath();
151+
Files.createDirectories(root.resolve("A"));
152+
Files.createDirectories(root.resolve("X/Y/Z"));
153+
Files.createDirectories(root.resolve("X/Y/D"));
154+
Files.createSymbolicLink(root.resolve("A/B"), root.resolve("X/Y/Z"));
155+
Files.createDirectories(root.resolve("A/B/C"));
156+
Files.createSymbolicLink(root.resolve("A/B/C/D"), Paths.get("../../D"));
157+
Files.createSymbolicLink(root.resolve("A/B/C/E"), Paths.get("../../Z"));
158+
}
159+
73160
/**
74161
* Test a very specific case of mutually recursive symbolic links:
75162
* <pre> {@code
@@ -85,7 +172,7 @@ protected void createBug358830Structure(IFileStore rootDir) throws CoreException
85172
*/
86173
@Test
87174
public void testBug232426() throws Exception {
88-
assumeTrue("only relevant for platforms supporting symbolic links", canCreateSymLinks());
175+
assumeTrue(canCreateSymLinks(), "only relevant for platforms supporting symbolic links");
89176

90177
IProject project = getWorkspace().getRoot().getProject("Project");
91178
createInWorkspace(project);
@@ -118,36 +205,42 @@ public boolean visit(IResource resource) {
118205
});
119206
}
120207

121-
@Test
122-
public void testBug358830() throws Exception {
123-
assumeTrue("only relevant for platforms supporting symbolic links", canCreateSymLinks());
124-
125-
IProject project = getWorkspace().getRoot().getProject("Project");
126-
createInWorkspace(project);
127-
/* Re-use projects which are cleaned up automatically */
128-
getWorkspace().run((IWorkspaceRunnable) monitor -> {
129-
/* delete open project because we must re-open with BACKGROUND_REFRESH */
130-
project.delete(IResource.NEVER_DELETE_PROJECT_CONTENT, createTestMonitor());
131-
project.create(null);
132-
try {
133-
createBug358830Structure(EFS.getStore(project.getLocationURI()));
134-
} catch (IOException e) {
135-
throw new IllegalStateException("unexpected IOException occurred", e);
136-
}
137-
project.open(IResource.BACKGROUND_REFRESH, createTestMonitor());
138-
}, null);
139-
140-
//wait for BACKGROUND_REFRESH to complete.
141-
waitForRefresh();
142-
final int resourceCount[] = new int[] {0};
143-
project.accept(resource -> {
144-
resourceCount[0]++;
145-
return true;
146-
});
147-
// We have 1 root + 1 folder + 1 file (.project)
148-
// + .settings / resources prefs
149-
// --> 5 elements to visit
150-
assertEquals(5, resourceCount[0]);
208+
@ParameterizedTest
209+
@ValueSource(booleans = { false, true })
210+
public void testBug358830(boolean useAdvancedLinkCheck) throws Exception {
211+
assumeTrue(canCreateSymLinks(), "only relevant for platforms supporting symbolic links");
212+
final boolean originalValue = UnifiedTree.isAdvancedRecursiveLinkChecksEnabled();
213+
try {
214+
UnifiedTree.enableAdvancedRecursiveLinkChecks(useAdvancedLinkCheck);
215+
IProject project = getWorkspace().getRoot().getProject("Project");
216+
createInWorkspace(project);
217+
/* Re-use projects which are cleaned up automatically */
218+
getWorkspace().run((IWorkspaceRunnable) monitor -> {
219+
/* delete open project because we must re-open with BACKGROUND_REFRESH */
220+
project.delete(IResource.NEVER_DELETE_PROJECT_CONTENT, createTestMonitor());
221+
project.create(null);
222+
try {
223+
createBug358830Structure(EFS.getStore(project.getLocationURI()));
224+
} catch (IOException e) {
225+
throw new IllegalStateException("unexpected IOException occurred", e);
226+
}
227+
project.open(IResource.BACKGROUND_REFRESH, createTestMonitor());
228+
}, null);
229+
230+
// wait for BACKGROUND_REFRESH to complete.
231+
waitForRefresh();
232+
final int resourceCount[] = new int[] { 0 };
233+
project.accept(resource -> {
234+
resourceCount[0]++;
235+
return true;
236+
});
237+
// We have 1 root + 1 folder + 1 file (.project)
238+
// + .settings / resources prefs
239+
// --> 5 elements to visit
240+
assertEquals(5, resourceCount[0]);
241+
} finally {
242+
UnifiedTree.enableAdvancedRecursiveLinkChecks(originalValue);
243+
}
151244
}
152245

153246
}

0 commit comments

Comments
 (0)