Skip to content

Commit d4c4f3a

Browse files
committed
Reflect broken symlinks in resources model
Broken file links were not present in resource model before, now they are (at least with Linux x86_64 LinuxFileHandler or Java NIO PosixHandler, in the future hopefully on all Unix platforms). For "broken" link overlays we need an additional patch in Platform UI. Fixes #2797
1 parent 7beef1c commit d4c4f3a

10 files changed

Lines changed: 273 additions & 29 deletions

File tree

‎resources/bundles/org.eclipse.core.filesystem/natives/unix/fastlinux/fastlinuxfile.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ JNIEXPORT jobjectArray JNICALL Java_org_eclipse_core_internal_filesystem_local_l
481481

482482
/* Follow symlink and update stat for further processing. */
483483
if (fstatat(directoryFd, entry->d_name, &st, 0) != 0) {
484-
memset(&st, 0, sizeof(st));
484+
/* Keep lstat data from the symlink itself for dangling links. */
485485
statErrno = errno;
486486
}
487487

‎resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*******************************************************************************/
1919
package org.eclipse.core.internal.filesystem.local;
2020

21+
import java.io.ByteArrayInputStream;
2122
import java.io.File;
2223
import java.io.FileInputStream;
2324
import java.io.FileNotFoundException;
@@ -533,17 +534,40 @@ public InputStream openInputStream(int options, IProgressMonitor monitor) throws
533534
try {
534535
return new FileInputStream(file);
535536
} catch (FileNotFoundException e) {
537+
IFileInfo info = fetchInfo();
538+
if (isBrokenSymbolicLink(info)) {
539+
// If the file is a symbolic link and the target is set but does not exist, return an empty
540+
// stream instead of throwing an exception
541+
return new ByteArrayInputStream(new byte[0]);
542+
}
536543
handleReadIOException(e);
537544
return null;
538545
}
539546
}
540547

548+
/**
549+
* @param info the file info to check
550+
* @return true if the file is a broken symbolic link, false otherwise
551+
*/
552+
private static boolean isBrokenSymbolicLink(IFileInfo info) {
553+
// Historically, file info for a broken symbolic links reported that it doesn't exist
554+
// if the target didn't existed as a file, which is actually incorrect.
555+
// We can check for the existence of the target by checking if the link target attribute is set.
556+
return !info.exists() && info.getAttribute(EFS.ATTRIBUTE_SYMLINK) && info.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET) != null;
557+
}
558+
541559
/** @see #openInputStream(int, IProgressMonitor) */
542560
@Override
543561
public byte[] readAllBytes(int options, IProgressMonitor monitor) throws CoreException {
544562
try {
545563
return Files.readAllBytes(file.toPath());
546564
} catch (IOException e) {
565+
IFileInfo info = fetchInfo();
566+
if (isBrokenSymbolicLink(info)) {
567+
// If the file is a symbolic link and the target is set but does not exist, return an empty
568+
// stream instead of throwing an exception
569+
return new byte[0];
570+
}
547571
handleReadIOException(e);
548572
return null;
549573
}

‎resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/linux/LinuxFileNatives.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,15 @@ public static FileInfo fetchFileInfo(String fileName) {
121121
info = new FileInfo();
122122
} else if ((stat.st_mode & S_IFMT) == S_IFLNK) { // it's a link!
123123
LinuxStructStat targetStat = new LinuxStructStat();
124-
if (stat(name, targetStat) == 0) { // get the information about the file the link points to
124+
if (stat(name, targetStat) == 0 && targetStat.errno == 0) { // get the information about the file the link points to
125125
info = targetStat.toFileInfo(); // store the target file stats in info
126126
} else { // invalid link target
127-
info = new FileInfo();
127+
// Keep symlink metadata (permissions/timestamp) from lstat for dangling links.
128+
info = stat.toFileInfo();
129+
if (targetStat.errno == ENOENT) {
130+
// Preserve historical "broken target" semantics while still providing link attributes.
131+
info.setExists(false);
132+
}
128133
if (targetStat.errno != ENOENT) {
129134
info.setError(IFileInfo.IO_ERROR);
130135
}

‎resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/PosixHandler.java‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616
package org.eclipse.core.internal.filesystem.local.nio;
1717

1818
import java.io.IOException;
19-
import java.nio.file.*;
20-
import java.nio.file.attribute.*;
19+
import java.nio.file.Files;
20+
import java.nio.file.LinkOption;
21+
import java.nio.file.NoSuchFileException;
22+
import java.nio.file.Path;
23+
import java.nio.file.Paths;
24+
import java.nio.file.attribute.PosixFileAttributeView;
25+
import java.nio.file.attribute.PosixFileAttributes;
26+
import java.nio.file.attribute.PosixFilePermission;
2127
import java.util.HashSet;
2228
import java.util.Set;
2329
import org.eclipse.core.filesystem.EFS;
@@ -51,14 +57,21 @@ public FileInfo fetchFileInfo(String fileName) {
5157

5258
try {
5359
PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
60+
info.setExists(true);
5461

5562
if (attrs.isSymbolicLink()) {
5663
info.setAttribute(EFS.ATTRIBUTE_SYMLINK, true);
5764
info.setStringAttribute(EFS.ATTRIBUTE_LINK_TARGET, Files.readSymbolicLink(path).toString());
58-
attrs = Files.readAttributes(path, PosixFileAttributes.class);
65+
try {
66+
attrs = Files.readAttributes(path, PosixFileAttributes.class);
67+
} catch (NoSuchFileException e) {
68+
// The link target does not exist, so we cannot read its attributes.
69+
// Keep symlink metadata (permissions/timestamp) for dangling links.
70+
// Preserve historical "broken target" semantics while still providing link attributes.
71+
info.setExists(false);
72+
}
5973
}
6074

61-
info.setExists(true);
6275
info.setLastModified(attrs.lastModifiedTime().toMillis());
6376
info.setLength(attrs.size());
6477
info.setDirectory(attrs.isDirectory());

‎resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ public IResource[] allResourcesFor(URI location, boolean files, int memberFlags)
329329
public ResourceAttributes attributes(IResource resource) {
330330
IFileStore store = getStore(resource);
331331
IFileInfo fileInfo = store.fetchInfo();
332-
if (!fileInfo.exists()) {
332+
if (!fileInfo.exists() && !fileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK)) {
333333
return null;
334334
}
335335
return FileUtil.fileInfoToAttributes(fileInfo);

‎resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTreeNode.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
package org.eclipse.core.internal.localstore;
1616

1717
import java.util.Iterator;
18-
import org.eclipse.core.filesystem.*;
18+
import org.eclipse.core.filesystem.EFS;
19+
import org.eclipse.core.filesystem.IFileInfo;
20+
import org.eclipse.core.filesystem.IFileStore;
1921
import org.eclipse.core.internal.resources.Resource;
2022
import org.eclipse.core.resources.IResource;
2123

@@ -41,7 +43,7 @@ public UnifiedTreeNode(UnifiedTree tree, IResource resource, IFileStore store, I
4143
}
4244

4345
public boolean existsInFileSystem() {
44-
return fileInfo != null && fileInfo.exists();
46+
return fileInfo != null && (fileInfo.exists() || fileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK));
4547
}
4648

4749
/**

‎resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,35 @@
1515
package org.eclipse.core.internal.resources;
1616

1717
import java.net.URI;
18-
import org.eclipse.core.filesystem.*;
18+
import org.eclipse.core.filesystem.EFS;
19+
import org.eclipse.core.filesystem.IFileInfo;
20+
import org.eclipse.core.filesystem.IFileStore;
21+
import org.eclipse.core.filesystem.IFileSystem;
1922
import org.eclipse.core.filesystem.URIUtil;
2023
import org.eclipse.core.internal.localstore.FileSystemResourceManager;
2124
import org.eclipse.core.internal.properties.IPropertyManager;
22-
import org.eclipse.core.internal.utils.*;
23-
import org.eclipse.core.resources.*;
25+
import org.eclipse.core.internal.utils.BitMask;
26+
import org.eclipse.core.internal.utils.Messages;
27+
import org.eclipse.core.internal.utils.Policy;
28+
import org.eclipse.core.resources.IContainer;
29+
import org.eclipse.core.resources.IFile;
30+
import org.eclipse.core.resources.IFolder;
31+
import org.eclipse.core.resources.IProject;
32+
import org.eclipse.core.resources.IProjectDescription;
33+
import org.eclipse.core.resources.IResource;
34+
import org.eclipse.core.resources.IResourceStatus;
35+
import org.eclipse.core.resources.IResourceVisitor;
36+
import org.eclipse.core.resources.ResourcesPlugin;
2437
import org.eclipse.core.resources.team.IResourceTree;
25-
import org.eclipse.core.runtime.*;
38+
import org.eclipse.core.runtime.Assert;
39+
import org.eclipse.core.runtime.CoreException;
40+
import org.eclipse.core.runtime.IPath;
41+
import org.eclipse.core.runtime.IProgressMonitor;
42+
import org.eclipse.core.runtime.IStatus;
43+
import org.eclipse.core.runtime.MultiStatus;
44+
import org.eclipse.core.runtime.NullProgressMonitor;
45+
import org.eclipse.core.runtime.OperationCanceledException;
46+
import org.eclipse.core.runtime.SubMonitor;
2647
import org.eclipse.core.runtime.jobs.ILock;
2748
import org.eclipse.osgi.util.NLS;
2849

@@ -289,7 +310,8 @@ private boolean internalDeleteFile(IFile file, int flags, IProgressMonitor monit
289310
// If the file doesn't exist on disk then signal to the workspace to delete the
290311
// file and return.
291312
IFileStore fileStore = localManager.getStore(file);
292-
boolean localExists = fileStore.fetchInfo().exists();
313+
IFileInfo localFileInfo = fileStore.fetchInfo();
314+
boolean localExists = localFileInfo.exists() || localFileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK);
293315
if (!localExists) {
294316
deletedFile(file);
295317
// Indicate that the delete was successful.

‎resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/SymlinkTest.java‎

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535

3636
import java.io.IOException;
3737
import java.io.OutputStream;
38+
import java.nio.file.Files;
39+
import java.nio.file.LinkOption;
40+
import java.nio.file.Paths;
41+
import java.nio.file.attribute.PosixFileAttributes;
3842
import java.util.function.Consumer;
3943
import java.util.function.Function;
4044
import org.eclipse.core.filesystem.EFS;
@@ -50,14 +54,28 @@
5054
import org.junit.jupiter.api.extension.RegisterExtension;
5155

5256
public class SymlinkTest {
57+
58+
public static final boolean SUPPORT_BROKEN_SYMLINKS = Platform.OS.isLinux()
59+
&& Platform.ARCH_X86_64.equals(Platform.getOSArch());
60+
61+
/**
62+
* Tolerance in milliseconds applied when comparing a file's last modified time against a
63+
* timestamp captured via {@link System#currentTimeMillis()}. The file system last modified
64+
* time may be provided by a finer-resolution native (JNI) source, so rounding/truncation can
65+
* make it appear a few milliseconds earlier than the wall-clock value.
66+
*/
67+
private static final long TIMESTAMP_TOLERANCE_MS = 1000;
68+
5369
/**
5470
* Symbolic links on Windows behave differently compared to Unix-based systems. Symbolic links
5571
* on Windows have their own set of attributes independent from the attributes of the link's
5672
* target. The {@link java.io.File#exists() File.exists()} method on Windows checks for
5773
* existence of the symbolic link itself, not its target.
5874
*/
5975
private static final boolean SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES = Platform.OS_WIN32
60-
.equals(Platform.getOS()) ? true : false;
76+
.equals(Platform.getOS());
77+
78+
6179
private static String specialCharName = "äöüß ÄÖÜ àÀâ µ²³úá"; //$NON-NLS-1$
6280

6381
protected IFileStore aDir, aFile; //actual Dir, File
@@ -109,7 +127,11 @@ protected void mkLink(IFileStore dir, String src, String tgt, boolean isDir) thr
109127

110128
@Test
111129
public void testBrokenSymlinkAttributes() throws Exception {
112-
long testStartTime = System.currentTimeMillis();
130+
// Use a tolerance because getLastModified() may originate from a finer-resolution
131+
// native (JNI) timer than System.currentTimeMillis(). Truncation/rounding can make
132+
// the reported modification time appear a few milliseconds before testStartTime even
133+
// though the file was created afterwards.
134+
long testStartTime = System.currentTimeMillis() - TIMESTAMP_TOLERANCE_MS;
113135
makeLinkStructure();
114136
//break links by removing actual dir and file
115137
ensureDoesNotExist(aDir);
@@ -124,22 +146,39 @@ public void testBrokenSymlinkAttributes() throws Exception {
124146
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, ilDir.isDirectory());
125147
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, illDir.exists());
126148
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, illDir.isDirectory());
127-
if (SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES) {
149+
if (SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS) {
128150
// Symlinks on Windows have their own modification time.
129-
assertTrue(ilFile.getLastModified() >= testStartTime);
130-
assertTrue(ilDir.getLastModified() >= testStartTime);
131-
assertTrue(illFile.getLastModified() >= testStartTime);
132-
assertTrue(illDir.getLastModified() >= testStartTime);
151+
assertTrue(ilFile.getLastModified() >= testStartTime,
152+
"getLastModified()=" + ilFile.getLastModified() + " testStartTime=" + testStartTime);
153+
assertTrue(ilDir.getLastModified() >= testStartTime,
154+
"getLastModified()=" + ilDir.getLastModified() + " testStartTime=" + testStartTime);
155+
assertTrue(illFile.getLastModified() >= testStartTime,
156+
"getLastModified()=" + illFile.getLastModified() + " testStartTime=" + testStartTime);
157+
assertTrue(illDir.getLastModified() >= testStartTime,
158+
"getLastModified()=" + illDir.getLastModified() + " testStartTime=" + testStartTime);
133159
} else {
134160
assertEquals(0, ilFile.getLastModified());
135161
assertEquals(0, ilDir.getLastModified());
136162
assertEquals(0, illFile.getLastModified());
137163
assertEquals(0, illDir.getLastModified());
138164
}
139-
assertEquals(0, ilFile.getLength());
140-
assertEquals(0, ilDir.getLength());
141-
assertEquals(0, illFile.getLength());
142-
assertEquals(0, illDir.getLength());
165+
166+
if (SUPPORT_BROKEN_SYMLINKS) {
167+
PosixFileAttributes ilFileAttrs = posixAttributess(lFile);
168+
PosixFileAttributes illFileAttrs = posixAttributess(llFile);
169+
PosixFileAttributes ilDirAttrs = posixAttributess(lDir);
170+
PosixFileAttributes illDirAttrs = posixAttributess(llDir);
171+
172+
assertEquals(ilFileAttrs.size(), ilFile.getLength());
173+
assertEquals(ilDirAttrs.size(), ilDir.getLength());
174+
assertEquals(illFileAttrs.size(), illFile.getLength());
175+
assertEquals(illDirAttrs.size(), illDir.getLength());
176+
} else {
177+
assertEquals(0, ilFile.getLength());
178+
assertEquals(0, ilDir.getLength());
179+
assertEquals(0, illFile.getLength());
180+
assertEquals(0, illDir.getLength());
181+
}
143182

144183
assertTrue(ilFile.getAttribute(EFS.ATTRIBUTE_SYMLINK));
145184
assertEquals(ilFile.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET), "aFile");
@@ -151,6 +190,11 @@ public void testBrokenSymlinkAttributes() throws Exception {
151190
assertEquals(illDir.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET), "lDir");
152191
}
153192

193+
private static PosixFileAttributes posixAttributess(IFileStore store) throws IOException {
194+
return Files.readAttributes(Paths.get(store.toURI()), PosixFileAttributes.class,
195+
LinkOption.NOFOLLOW_LINKS);
196+
}
197+
154198
// Moving a broken symlink is possible.
155199
@Test
156200
public void testBrokenSymlinkMove() throws Exception {
@@ -213,7 +257,7 @@ public void testRecursiveSymlink() throws Exception {
213257
mkLink(baseStore, "l2", "l1", false);
214258
IFileStore l1 = baseStore.getChild("l1");
215259
IFileInfo i1 = l1.fetchInfo();
216-
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, i1.exists());
260+
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS, i1.exists());
217261
assertFalse(i1.isDirectory());
218262

219263
assertTrue(i1.getAttribute(EFS.ATTRIBUTE_SYMLINK));
@@ -235,7 +279,7 @@ public void testRecursiveSymlink() throws Exception {
235279
assertTrue(exceptionThrown);
236280
assertTrue(i1.getAttribute(EFS.ATTRIBUTE_READ_ONLY));
237281
}
238-
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, i1.exists());
282+
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS, i1.exists());
239283

240284
i1.setLastModified(12345);
241285
exceptionThrown = false;
@@ -248,7 +292,7 @@ public void testRecursiveSymlink() throws Exception {
248292
//FIXME bug: putInfo neither sets attributes nor throws an exception for broken symbolic links
249293
//assertTrue(exceptionThrown);
250294
//assertEquals(i1.getLastModified(), 12345);
251-
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, i1.exists());
295+
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS, i1.exists());
252296

253297
l1.delete(EFS.NONE, getMonitor());
254298
infos = baseStore.childInfos(EFS.NONE, getMonitor());

0 commit comments

Comments
 (0)