diff --git a/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/META-INF/MANIFEST.MF b/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/META-INF/MANIFEST.MF
index c87b4505a56..6931428e8f8 100644
--- a/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/META-INF/MANIFEST.MF
+++ b/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %fragmentName
Bundle-SymbolicName: org.eclipse.core.filesystem.linux.x86_64; singleton:=true
-Bundle-Version: 1.2.500.qualifier
+Bundle-Version: 1.2.600.qualifier
Bundle-Vendor: %providerName
Fragment-Host: org.eclipse.core.filesystem;bundle-version="[1.11.500,2.0.0)"
Bundle-Localization: fragment
diff --git a/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/os/linux/x86_64/libfastlinuxfile_1_0_0.so b/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/os/linux/x86_64/libfastlinuxfile_1_0_0.so
index 8246930cd6a..ea56e3c1eba 100644
Binary files a/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/os/linux/x86_64/libfastlinuxfile_1_0_0.so and b/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/os/linux/x86_64/libfastlinuxfile_1_0_0.so differ
diff --git a/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/pom.xml b/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/pom.xml
index c44e07f8d4b..656c1f67019 100644
--- a/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/pom.xml
+++ b/resources/bundles/org.eclipse.core.filesystem.linux.x86_64/pom.xml
@@ -18,7 +18,7 @@
../../
org.eclipse.core.filesystem.linux.x86_64
- 1.2.500-SNAPSHOT
+ 1.2.600-SNAPSHOT
eclipse-plugin
diff --git a/resources/bundles/org.eclipse.core.filesystem/META-INF/MANIFEST.MF b/resources/bundles/org.eclipse.core.filesystem/META-INF/MANIFEST.MF
index 5bea0c13567..46aaaccaef0 100644
--- a/resources/bundles/org.eclipse.core.filesystem/META-INF/MANIFEST.MF
+++ b/resources/bundles/org.eclipse.core.filesystem/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.core.filesystem; singleton:=true
-Bundle-Version: 1.11.500.qualifier
+Bundle-Version: 1.11.600.qualifier
Bundle-Localization: plugin
Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)"
Export-Package: org.eclipse.core.filesystem;uses:="org.eclipse.core.runtime",
diff --git a/resources/bundles/org.eclipse.core.filesystem/natives/unix/fastlinux/fastlinuxfile.c b/resources/bundles/org.eclipse.core.filesystem/natives/unix/fastlinux/fastlinuxfile.c
index 6bc4238134a..98d3b70f9be 100644
--- a/resources/bundles/org.eclipse.core.filesystem/natives/unix/fastlinux/fastlinuxfile.c
+++ b/resources/bundles/org.eclipse.core.filesystem/natives/unix/fastlinux/fastlinuxfile.c
@@ -481,7 +481,7 @@ JNIEXPORT jobjectArray JNICALL Java_org_eclipse_core_internal_filesystem_local_l
/* Follow symlink and update stat for further processing. */
if (fstatat(directoryFd, entry->d_name, &st, 0) != 0) {
- memset(&st, 0, sizeof(st));
+ /* Keep lstat data from the symlink itself for dangling links. */
statErrno = errno;
}
diff --git a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java
index 3fc03131d99..2fa360d4fac 100644
--- a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java
+++ b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/LocalFile.java
@@ -18,6 +18,7 @@
*******************************************************************************/
package org.eclipse.core.internal.filesystem.local;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -533,17 +534,40 @@ public InputStream openInputStream(int options, IProgressMonitor monitor) throws
try {
return new FileInputStream(file);
} catch (FileNotFoundException e) {
+ IFileInfo info = fetchInfo();
+ if (isBrokenSymbolicLink(info)) {
+ // If the file is a symbolic link and the target is set but does not exist, return an empty
+ // stream instead of throwing an exception
+ return new ByteArrayInputStream(new byte[0]);
+ }
handleReadIOException(e);
return null;
}
}
+ /**
+ * @param info the file info to check
+ * @return true if the file is a broken symbolic link, false otherwise
+ */
+ private static boolean isBrokenSymbolicLink(IFileInfo info) {
+ // Historically, file info for a broken symbolic links reported that it doesn't exist
+ // if the target didn't existed as a file, which is actually incorrect.
+ // We can check for the existence of the target by checking if the link target attribute is set.
+ return !info.exists() && info.getAttribute(EFS.ATTRIBUTE_SYMLINK) && info.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET) != null;
+ }
+
/** @see #openInputStream(int, IProgressMonitor) */
@Override
public byte[] readAllBytes(int options, IProgressMonitor monitor) throws CoreException {
try {
return Files.readAllBytes(file.toPath());
} catch (IOException e) {
+ IFileInfo info = fetchInfo();
+ if (isBrokenSymbolicLink(info)) {
+ // If the file is a symbolic link and the target is set but does not exist, return an empty
+ // stream instead of throwing an exception
+ return new byte[0];
+ }
handleReadIOException(e);
return null;
}
diff --git a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/linux/LinuxFileNatives.java b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/linux/LinuxFileNatives.java
index a50462736e2..b8995b3dd67 100644
--- a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/linux/LinuxFileNatives.java
+++ b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/linux/LinuxFileNatives.java
@@ -121,10 +121,15 @@ public static FileInfo fetchFileInfo(String fileName) {
info = new FileInfo();
} else if ((stat.st_mode & S_IFMT) == S_IFLNK) { // it's a link!
LinuxStructStat targetStat = new LinuxStructStat();
- if (stat(name, targetStat) == 0) { // get the information about the file the link points to
+ if (stat(name, targetStat) == 0 && targetStat.errno == 0) { // get the information about the file the link points to
info = targetStat.toFileInfo(); // store the target file stats in info
} else { // invalid link target
- info = new FileInfo();
+ // Keep symlink metadata (permissions/timestamp) from lstat for dangling links.
+ info = stat.toFileInfo();
+ if (targetStat.errno == ENOENT) {
+ // Preserve historical "broken target" semantics while still providing link attributes.
+ info.setExists(false);
+ }
if (targetStat.errno != ENOENT) {
info.setError(IFileInfo.IO_ERROR);
}
diff --git a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/PosixHandler.java b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/PosixHandler.java
index 55ec5ac3104..28dbc856328 100644
--- a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/PosixHandler.java
+++ b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/PosixHandler.java
@@ -16,8 +16,14 @@
package org.eclipse.core.internal.filesystem.local.nio;
import java.io.IOException;
-import java.nio.file.*;
-import java.nio.file.attribute.*;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.core.filesystem.EFS;
@@ -51,14 +57,21 @@ public FileInfo fetchFileInfo(String fileName) {
try {
PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ info.setExists(true);
if (attrs.isSymbolicLink()) {
info.setAttribute(EFS.ATTRIBUTE_SYMLINK, true);
info.setStringAttribute(EFS.ATTRIBUTE_LINK_TARGET, Files.readSymbolicLink(path).toString());
- attrs = Files.readAttributes(path, PosixFileAttributes.class);
+ try {
+ attrs = Files.readAttributes(path, PosixFileAttributes.class);
+ } catch (NoSuchFileException e) {
+ // The link target does not exist, so we cannot read its attributes.
+ // Keep symlink metadata (permissions/timestamp) for dangling links.
+ // Preserve historical "broken target" semantics while still providing link attributes.
+ info.setExists(false);
+ }
}
- info.setExists(true);
info.setLastModified(attrs.lastModifiedTime().toMillis());
info.setLength(attrs.size());
info.setDirectory(attrs.isDirectory());
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java
index e1d3fff3a8e..0f99318d17f 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/FileSystemResourceManager.java
@@ -329,7 +329,7 @@ public IResource[] allResourcesFor(URI location, boolean files, int memberFlags)
public ResourceAttributes attributes(IResource resource) {
IFileStore store = getStore(resource);
IFileInfo fileInfo = store.fetchInfo();
- if (!fileInfo.exists()) {
+ if (!fileInfo.exists() && !fileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK)) {
return null;
}
return FileUtil.fileInfoToAttributes(fileInfo);
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTreeNode.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTreeNode.java
index 5c6de678e21..8c0b434317f 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTreeNode.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTreeNode.java
@@ -15,7 +15,9 @@
package org.eclipse.core.internal.localstore;
import java.util.Iterator;
-import org.eclipse.core.filesystem.*;
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileInfo;
+import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.resources.Resource;
import org.eclipse.core.resources.IResource;
@@ -41,7 +43,7 @@ public UnifiedTreeNode(UnifiedTree tree, IResource resource, IFileStore store, I
}
public boolean existsInFileSystem() {
- return fileInfo != null && fileInfo.exists();
+ return fileInfo != null && (fileInfo.exists() || fileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK));
}
/**
diff --git a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java
index cac927a0644..7b95c9374ca 100644
--- a/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java
+++ b/resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/ResourceTree.java
@@ -15,14 +15,35 @@
package org.eclipse.core.internal.resources;
import java.net.URI;
-import org.eclipse.core.filesystem.*;
+import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileInfo;
+import org.eclipse.core.filesystem.IFileStore;
+import org.eclipse.core.filesystem.IFileSystem;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.internal.localstore.FileSystemResourceManager;
import org.eclipse.core.internal.properties.IPropertyManager;
-import org.eclipse.core.internal.utils.*;
-import org.eclipse.core.resources.*;
+import org.eclipse.core.internal.utils.BitMask;
+import org.eclipse.core.internal.utils.Messages;
+import org.eclipse.core.internal.utils.Policy;
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceStatus;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.team.IResourceTree;
-import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.MultiStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.osgi.util.NLS;
@@ -289,7 +310,8 @@ private boolean internalDeleteFile(IFile file, int flags, IProgressMonitor monit
// If the file doesn't exist on disk then signal to the workspace to delete the
// file and return.
IFileStore fileStore = localManager.getStore(file);
- boolean localExists = fileStore.fetchInfo().exists();
+ IFileInfo localFileInfo = fileStore.fetchInfo();
+ boolean localExists = localFileInfo.exists() || localFileInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK);
if (!localExists) {
deletedFile(file);
// Indicate that the delete was successful.
diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/SymlinkTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/SymlinkTest.java
index 421fe599257..cd8bc1d5d59 100644
--- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/SymlinkTest.java
+++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/filesystem/SymlinkTest.java
@@ -35,6 +35,10 @@
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributes;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.core.filesystem.EFS;
@@ -50,6 +54,18 @@
import org.junit.jupiter.api.extension.RegisterExtension;
public class SymlinkTest {
+
+ public static final boolean SUPPORT_BROKEN_SYMLINKS = Platform.OS.isLinux()
+ && Platform.ARCH_X86_64.equals(Platform.getOSArch());
+
+ /**
+ * Tolerance in milliseconds applied when comparing a file's last modified time against a
+ * timestamp captured via {@link System#currentTimeMillis()}. The file system last modified
+ * time may be provided by a finer-resolution native (JNI) source, so rounding/truncation can
+ * make it appear a few milliseconds earlier than the wall-clock value.
+ */
+ private static final long TIMESTAMP_TOLERANCE_MS = 1000;
+
/**
* Symbolic links on Windows behave differently compared to Unix-based systems. Symbolic links
* on Windows have their own set of attributes independent from the attributes of the link's
@@ -57,7 +73,9 @@ public class SymlinkTest {
* existence of the symbolic link itself, not its target.
*/
private static final boolean SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES = Platform.OS_WIN32
- .equals(Platform.getOS()) ? true : false;
+ .equals(Platform.getOS());
+
+
private static String specialCharName = "äöüß ÄÖÜ àÀâ µ²³úá"; //$NON-NLS-1$
protected IFileStore aDir, aFile; //actual Dir, File
@@ -109,7 +127,11 @@ protected void mkLink(IFileStore dir, String src, String tgt, boolean isDir) thr
@Test
public void testBrokenSymlinkAttributes() throws Exception {
- long testStartTime = System.currentTimeMillis();
+ // Use a tolerance because getLastModified() may originate from a finer-resolution
+ // native (JNI) timer than System.currentTimeMillis(). Truncation/rounding can make
+ // the reported modification time appear a few milliseconds before testStartTime even
+ // though the file was created afterwards.
+ long testStartTime = System.currentTimeMillis() - TIMESTAMP_TOLERANCE_MS;
makeLinkStructure();
//break links by removing actual dir and file
ensureDoesNotExist(aDir);
@@ -124,22 +146,39 @@ public void testBrokenSymlinkAttributes() throws Exception {
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, ilDir.isDirectory());
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, illDir.exists());
assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, illDir.isDirectory());
- if (SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES) {
+ if (SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS) {
// Symlinks on Windows have their own modification time.
- assertTrue(ilFile.getLastModified() >= testStartTime);
- assertTrue(ilDir.getLastModified() >= testStartTime);
- assertTrue(illFile.getLastModified() >= testStartTime);
- assertTrue(illDir.getLastModified() >= testStartTime);
+ assertTrue(ilFile.getLastModified() >= testStartTime,
+ "getLastModified()=" + ilFile.getLastModified() + " testStartTime=" + testStartTime);
+ assertTrue(ilDir.getLastModified() >= testStartTime,
+ "getLastModified()=" + ilDir.getLastModified() + " testStartTime=" + testStartTime);
+ assertTrue(illFile.getLastModified() >= testStartTime,
+ "getLastModified()=" + illFile.getLastModified() + " testStartTime=" + testStartTime);
+ assertTrue(illDir.getLastModified() >= testStartTime,
+ "getLastModified()=" + illDir.getLastModified() + " testStartTime=" + testStartTime);
} else {
assertEquals(0, ilFile.getLastModified());
assertEquals(0, ilDir.getLastModified());
assertEquals(0, illFile.getLastModified());
assertEquals(0, illDir.getLastModified());
}
- assertEquals(0, ilFile.getLength());
- assertEquals(0, ilDir.getLength());
- assertEquals(0, illFile.getLength());
- assertEquals(0, illDir.getLength());
+
+ if (SUPPORT_BROKEN_SYMLINKS) {
+ PosixFileAttributes ilFileAttrs = posixAttributess(lFile);
+ PosixFileAttributes illFileAttrs = posixAttributess(llFile);
+ PosixFileAttributes ilDirAttrs = posixAttributess(lDir);
+ PosixFileAttributes illDirAttrs = posixAttributess(llDir);
+
+ assertEquals(ilFileAttrs.size(), ilFile.getLength());
+ assertEquals(ilDirAttrs.size(), ilDir.getLength());
+ assertEquals(illFileAttrs.size(), illFile.getLength());
+ assertEquals(illDirAttrs.size(), illDir.getLength());
+ } else {
+ assertEquals(0, ilFile.getLength());
+ assertEquals(0, ilDir.getLength());
+ assertEquals(0, illFile.getLength());
+ assertEquals(0, illDir.getLength());
+ }
assertTrue(ilFile.getAttribute(EFS.ATTRIBUTE_SYMLINK));
assertEquals(ilFile.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET), "aFile");
@@ -151,6 +190,11 @@ public void testBrokenSymlinkAttributes() throws Exception {
assertEquals(illDir.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET), "lDir");
}
+ private static PosixFileAttributes posixAttributess(IFileStore store) throws IOException {
+ return Files.readAttributes(Paths.get(store.toURI()), PosixFileAttributes.class,
+ LinkOption.NOFOLLOW_LINKS);
+ }
+
// Moving a broken symlink is possible.
@Test
public void testBrokenSymlinkMove() throws Exception {
@@ -213,7 +257,7 @@ public void testRecursiveSymlink() throws Exception {
mkLink(baseStore, "l2", "l1", false);
IFileStore l1 = baseStore.getChild("l1");
IFileInfo i1 = l1.fetchInfo();
- assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, i1.exists());
+ assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS, i1.exists());
assertFalse(i1.isDirectory());
assertTrue(i1.getAttribute(EFS.ATTRIBUTE_SYMLINK));
@@ -235,7 +279,7 @@ public void testRecursiveSymlink() throws Exception {
assertTrue(exceptionThrown);
assertTrue(i1.getAttribute(EFS.ATTRIBUTE_READ_ONLY));
}
- assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, i1.exists());
+ assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS, i1.exists());
i1.setLastModified(12345);
exceptionThrown = false;
@@ -248,7 +292,7 @@ public void testRecursiveSymlink() throws Exception {
//FIXME bug: putInfo neither sets attributes nor throws an exception for broken symbolic links
//assertTrue(exceptionThrown);
//assertEquals(i1.getLastModified(), 12345);
- assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES, i1.exists());
+ assertEquals(SYMLINKS_ARE_FIRST_CLASS_FILES_OR_DIRECTORIES || SUPPORT_BROKEN_SYMLINKS, i1.exists());
l1.delete(EFS.NONE, getMonitor());
infos = baseStore.childInfos(EFS.NONE, getMonitor());
diff --git a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/SymlinkResourceTest.java b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/SymlinkResourceTest.java
index 288a0ee4cd2..4310808c6dc 100644
--- a/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/SymlinkResourceTest.java
+++ b/resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/SymlinkResourceTest.java
@@ -22,23 +22,36 @@
import static org.eclipse.core.tests.resources.ResourceTestUtil.createTestMonitor;
import static org.eclipse.core.tests.resources.ResourceTestUtil.waitForRefresh;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.file.Files;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
import org.eclipse.core.filesystem.EFS;
+import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.localstore.UnifiedTree;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform.OS;
+import org.eclipse.core.tests.filesystem.SymlinkTest;
import org.eclipse.core.tests.resources.util.WorkspaceResetExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -244,4 +257,125 @@ public void testBug358830(boolean useAdvancedLinkCheck) throws Exception {
}
}
+ @Test
+ public void testBrokenSymlinkSeenAsIFileChild() throws Exception {
+ assumeTrue(canCreateSymLinks() && supportsBrokenSymlinks(),
+ "only relevant for platforms supporting symbolic links");
+
+ IProject project = getWorkspace().getRoot().getProject("testBrokenSymlinkSeenAsIFileChild");
+ createInWorkspace(project);
+
+ IFileStore folderStore = EFS.getStore(project.getLocationURI()).getChild("parent");
+ folderStore.mkdir(EFS.NONE, createTestMonitor());
+ mkLink(folderStore, "broken.txt", "missing.txt", false);
+
+ project.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor());
+
+ IFolder parent = project.getFolder("parent");
+ IFile brokenLink = project.getFile("parent/broken.txt");
+ assertTrue(parent.exists());
+ assertTrue(brokenLink.exists());
+ assertEquals(IResource.FILE, brokenLink.getType());
+
+ try (InputStream contents = brokenLink.getContents()) {
+ assertNotNull(contents);
+ assertEquals(0, contents.readAllBytes().length);
+ }
+
+ // A broken symlink is exposed as an empty file. All read-oriented IFile
+ // API must handle it gracefully (empty content / sensible defaults)
+ // rather than fail.
+ try (InputStream forcedContents = brokenLink.getContents(true)) {
+ assertNotNull(forcedContents);
+ assertEquals(0, forcedContents.readAllBytes().length);
+ }
+ assertEquals(0, brokenLink.readAllBytes().length);
+ assertEquals(0, brokenLink.readNBytes(1024).length);
+ assertEquals(0, brokenLink.readAllChars().length);
+ assertEquals("", brokenLink.readString());
+
+ // Charset / encoding lookups must not fail for a broken link.
+ assertNotNull(brokenLink.getCharset());
+ assertNotNull(brokenLink.getCharset(true));
+ // getCharset(false) may return null when no charset was explicitly set;
+ // it just must not throw.
+ assertNull(brokenLink.getCharset(false));
+
+ // Content description of an empty file must be obtainable without
+ // throwing (the value itself may be null when no type can be inferred).
+ brokenLink.getContentDescription();
+
+ // Line separator lookup must not fail.
+ assertNotNull(brokenLink.getLineSeparator(true));
+
+ // A broken link has no local history and is not content-restricted.
+ assertEquals(0, brokenLink.getHistory(createTestMonitor()).length);
+ assertFalse(brokenLink.isContentRestricted());
+
+ IFileStore fileStore = EFS.getStore(brokenLink.getLocationURI());
+ IFileInfo info = fileStore.fetchInfo();
+
+ ResourceAttributes resourceAttributes = brokenLink.getResourceAttributes();
+ assertNotNull(resourceAttributes);
+ assertTrue(resourceAttributes.isSymbolicLink());
+ assertFalse(resourceAttributes.isReadOnly());
+ assertTrue(resourceAttributes.isExecutable());
+ assertFalse(resourceAttributes.isHidden());
+ assertFalse(resourceAttributes.isArchive());
+
+ // java.io.File follows symlinks and thus has no data for broken links.
+ // Use java.nio.file with NOFOLLOW_LINKS (lstat semantics) to obtain the
+ // symlink's own metadata and validate the attributes reported by EFS.
+ Path linkPath = brokenLink.getLocation().toFile().toPath();
+ assertTrue(Files.isSymbolicLink(linkPath));
+ PosixFileAttributes linkAttributes = Files.readAttributes(linkPath, PosixFileAttributes.class,
+ LinkOption.NOFOLLOW_LINKS);
+
+ // symlink attribute and link target
+ assertTrue(info.getAttribute(EFS.ATTRIBUTE_SYMLINK));
+ assertEquals(Files.readSymbolicLink(linkPath).toString(), info.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET));
+
+ // length and modification time come from the link node itself
+ assertEquals(linkAttributes.size(), info.getLength());
+ assertEquals(linkAttributes.lastModifiedTime().toMillis() / 1000, info.getLastModified() / 1000);
+
+ // permissions come from the link node itself
+ Set permissions = linkAttributes.permissions();
+ assertEquals(permissions.contains(PosixFilePermission.OWNER_READ),
+ info.getAttribute(EFS.ATTRIBUTE_OWNER_READ));
+ assertEquals(permissions.contains(PosixFilePermission.OWNER_WRITE),
+ info.getAttribute(EFS.ATTRIBUTE_OWNER_WRITE));
+ assertEquals(permissions.contains(PosixFilePermission.OWNER_EXECUTE),
+ info.getAttribute(EFS.ATTRIBUTE_OWNER_EXECUTE));
+ assertEquals(permissions.contains(PosixFilePermission.GROUP_READ),
+ info.getAttribute(EFS.ATTRIBUTE_GROUP_READ));
+ assertEquals(permissions.contains(PosixFilePermission.GROUP_WRITE),
+ info.getAttribute(EFS.ATTRIBUTE_GROUP_WRITE));
+ assertEquals(permissions.contains(PosixFilePermission.GROUP_EXECUTE),
+ info.getAttribute(EFS.ATTRIBUTE_GROUP_EXECUTE));
+ assertEquals(permissions.contains(PosixFilePermission.OTHERS_READ),
+ info.getAttribute(EFS.ATTRIBUTE_OTHER_READ));
+ assertEquals(permissions.contains(PosixFilePermission.OTHERS_WRITE),
+ info.getAttribute(EFS.ATTRIBUTE_OTHER_WRITE));
+ assertEquals(permissions.contains(PosixFilePermission.OTHERS_EXECUTE),
+ info.getAttribute(EFS.ATTRIBUTE_OTHER_EXECUTE));
+
+ boolean foundAsIFileChild = false;
+ for (IResource member : parent.members()) {
+ if ("broken.txt".equals(member.getName())) {
+ foundAsIFileChild = true;
+ assertTrue(member instanceof IFile);
+ assertEquals(IResource.FILE, member.getType());
+ assertTrue(member.exists());
+ assertNotNull(member.getResourceAttributes());
+ assertTrue(member.getResourceAttributes().isSymbolicLink());
+ }
+ }
+ assertTrue(foundAsIFileChild, "Expected parent folder to contain broken symlink as child IFile");
+ }
+
+ private boolean supportsBrokenSymlinks() {
+ return SymlinkTest.SUPPORT_BROKEN_SYMLINKS;
+ }
+
}