diff --git a/resources/bundles/org.eclipse.core.filesystem/.classpath b/resources/bundles/org.eclipse.core.filesystem/.classpath
index 81fe078c20c..408b0704049 100644
--- a/resources/bundles/org.eclipse.core.filesystem/.classpath
+++ b/resources/bundles/org.eclipse.core.filesystem/.classpath
@@ -1,7 +1,16 @@
These are standard POSIX constants as defined in {@code
This handler delegates to {@link LinuxFileNatives}, which calls + * {@code statx(2)}, {@code chmod(2)} and {@code readlink(2)} directly via + * FFM downcall handles. No native shared library (.so) is required — the + * standard C library (libc) is used directly.
+ * + *Mac OS X specific code paths present in the old {@code UnixFileHandler} + * (chflags, UF_IMMUTABLE, SF_IMMUTABLE, CoreServices unicode conversion) are + * intentionally absent. This handler is exclusively for Linux.
+ * + * @since 1.11.500 + * @see LinuxFileNatives + */ +public class LinuxFileHandler extends NativeHandler { + + @Override + public int getSupportedAttributes() { + return LinuxFileNatives.getSupportedAttributes(); + } + + @Override + public FileInfo fetchFileInfo(String fileName) { + return LinuxFileNatives.fetchFileInfo(fileName); + } + + @Override + public boolean putFileInfo(String fileName, IFileInfo info, int options) { + return LinuxFileNatives.putFileInfo(fileName, info, options); + } + +} diff --git a/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileNatives.java b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileNatives.java new file mode 100644 index 00000000000..4151e93a01a --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileNatives.java @@ -0,0 +1,318 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.filesystem.linux; + +import java.io.File; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.charset.StandardCharsets; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.provider.FileInfo; + +/** + * Provides native file operations for Linux using the Java 25 Foreign Function & + * Memory (FFM) API instead of JNI. + * + *This class calls the following libc functions via FFM downcall handles:
+ *No Mac OS X specific code (chflags, UF_IMMUTABLE, SF_IMMUTABLE, tounicode, + * CoreServices) is present. File names are always encoded as UTF-8, which is the + * standard on modern Linux.
+ * + * @since 1.11.500 + */ +public final class LinuxFileNatives { + + // ---------- statx(2) flags ------------------------------------------------- + + /** {@code AT_FDCWD} — interpret pathname relative to the current working directory. */ + private static final int AT_FDCWD = -100; + + /** {@code AT_SYMLINK_NOFOLLOW} — do not follow the final symlink in pathname. */ + private static final int AT_SYMLINK_NOFOLLOW = 0x100; + + /** + * {@code STATX_BASIC_STATS} — request all basic stat fields + * ({@code stx_mode}, {@code stx_size}, {@code stx_mtime}, etc.). + */ + private static final int STATX_BASIC_STATS = 0x07FF; + + /** {@code ENOENT} errno value: no such file or directory. */ + private static final int ENOENT = 2; + + // ---------- statx struct offsets ------------------------------------------- + // + // The statx struct layout is defined inFollows symlinks for the file metadata ({@code stat} semantics) but + * also reports whether the path itself is a symlink and its target + * ({@code lstat} semantics for the link itself).
+ * + * @param fileName the absolute path of the file + * @return a {@link FileInfo} populated from the file's stat data + */ + public static FileInfo fetchFileInfo(String fileName) { + FileInfo info = null; + + try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(CAPTURED_STATE_LAYOUT); + MemorySegment nameSeg = arena.allocateFrom(fileName, StandardCharsets.UTF_8); + MemorySegment statxBuf = arena.allocate(STATX_STRUCT_SIZE, 8L); + + // First call: lstat semantics (AT_SYMLINK_NOFOLLOW) to detect symlinks + int result = (int) STATX_MH.invoke(capturedState, AT_FDCWD, nameSeg, + AT_SYMLINK_NOFOLLOW, STATX_BASIC_STATS, statxBuf); + + if (result == 0) { + int mode = Short.toUnsignedInt(statxBuf.get(ValueLayout.JAVA_SHORT, STATX_MODE_OFFSET)); + + if ((mode & LinuxFileFlags.S_IFMT) == LinuxFileFlags.S_IFLNK) { + // The path is a symlink: follow it to get the target's metadata + MemorySegment targetBuf = arena.allocate(STATX_STRUCT_SIZE, 8L); + int followResult = (int) STATX_MH.invoke(capturedState, AT_FDCWD, nameSeg, + 0 /* follow symlinks */, STATX_BASIC_STATS, targetBuf); + + if (followResult == 0) { + info = statxBufToFileInfo(targetBuf); + } else { + // Broken symlink (target does not exist) + info = new FileInfo(); + int errno = (int) ERRNO_VH.get(capturedState, 0L); + if (errno != ENOENT) { + info.setError(IFileInfo.IO_ERROR); + } + } + info.setAttribute(EFS.ATTRIBUTE_SYMLINK, true); + + // Read the symlink target string + MemorySegment linkBuf = arena.allocate(LinuxFileFlags.PATH_MAX); + long len = (long) READLINK_MH.invoke(capturedState, nameSeg, linkBuf, + (long) LinuxFileFlags.PATH_MAX); + if (len > 0) { + byte[] linkBytes = linkBuf.asSlice(0L, len).toArray(ValueLayout.JAVA_BYTE); + info.setStringAttribute(EFS.ATTRIBUTE_LINK_TARGET, + new String(linkBytes, StandardCharsets.UTF_8)); + } + } else { + info = statxBufToFileInfo(statxBuf); + } + } else { + info = new FileInfo(); + int errno = (int) ERRNO_VH.get(capturedState, 0L); + if (errno != ENOENT) { + info.setError(IFileInfo.IO_ERROR); + } + } + } catch (Throwable e) { + info = new FileInfo(); + info.setError(IFileInfo.IO_ERROR); + } + + if (info.getName() == null) { + // Use the basename of the supplied path as the file name + info.setName(new File(fileName).getName()); + } + return info; + } + + /** + * Updates the file permissions for the given path. + * + * @param fileName the absolute path of the file + * @param info the desired file attributes + * @param options (unused; reserved for future use) + * @return {@code true} if the chmod call succeeded + */ + public static boolean putFileInfo(String fileName, IFileInfo info, int options) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment capturedState = arena.allocate(CAPTURED_STATE_LAYOUT); + MemorySegment nameSeg = arena.allocateFrom(fileName, StandardCharsets.UTF_8); + + int mode = 0; + if (info.getAttribute(EFS.ATTRIBUTE_OWNER_READ)) mode |= LinuxFileFlags.S_IRUSR; + if (info.getAttribute(EFS.ATTRIBUTE_OWNER_WRITE)) mode |= LinuxFileFlags.S_IWUSR; + if (info.getAttribute(EFS.ATTRIBUTE_OWNER_EXECUTE)) mode |= LinuxFileFlags.S_IXUSR; + if (info.getAttribute(EFS.ATTRIBUTE_GROUP_READ)) mode |= LinuxFileFlags.S_IRGRP; + if (info.getAttribute(EFS.ATTRIBUTE_GROUP_WRITE)) mode |= LinuxFileFlags.S_IWGRP; + if (info.getAttribute(EFS.ATTRIBUTE_GROUP_EXECUTE)) mode |= LinuxFileFlags.S_IXGRP; + if (info.getAttribute(EFS.ATTRIBUTE_OTHER_READ)) mode |= LinuxFileFlags.S_IROTH; + if (info.getAttribute(EFS.ATTRIBUTE_OTHER_WRITE)) mode |= LinuxFileFlags.S_IWOTH; + if (info.getAttribute(EFS.ATTRIBUTE_OTHER_EXECUTE)) mode |= LinuxFileFlags.S_IXOTH; + + int code = (int) CHMOD_MH.invoke(capturedState, nameSeg, mode); + return code == 0; + } catch (Throwable e) { + return false; + } + } + + // ---------- Private helpers ------------------------------------------------ + + /** + * Reads the mode, size and mtime fields from a {@code struct statx} memory + * segment and converts them into a {@link FileInfo}. + */ + private static FileInfo statxBufToFileInfo(MemorySegment statxBuf) { + int mode = Short.toUnsignedInt(statxBuf.get(ValueLayout.JAVA_SHORT, STATX_MODE_OFFSET)); + long size = statxBuf.get(ValueLayout.JAVA_LONG, STATX_SIZE_OFFSET); + long mtimeSec = statxBuf.get(ValueLayout.JAVA_LONG, STATX_MTIME_SEC_OFFSET); + long mtimeNsec = Integer.toUnsignedLong(statxBuf.get(ValueLayout.JAVA_INT, STATX_MTIME_NSEC_OFFSET)); + return new LinuxStructStat(mode, size, mtimeSec, mtimeNsec).toFileInfo(); + } + + private LinuxFileNatives() { + // not instantiable + } + +} diff --git a/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxStructStat.java b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxStructStat.java new file mode 100644 index 00000000000..258f9ed5a4e --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxStructStat.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.filesystem.linux; + +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.provider.FileInfo; + +/** + * Holds the relevant fields from the Linux {@code struct stat} obtained via + * {@code statx(2)} and converts them to an Eclipse {@link FileInfo}. + * + *Unlike the shared {@code StructStat} used by the JNI/Unix implementation, + * this class contains no Mac OS X specific fields (no {@code st_flags}).
+ * + * @since 1.11.500 + */ +public class LinuxStructStat { + + private static final boolean USE_MILLISECOND_RESOLUTION = Boolean.parseBoolean( + System.getProperty("eclipse.filesystem.useNatives.modificationTimestampMillisecondsResolution", //$NON-NLS-1$ + "true")); //$NON-NLS-1$ + + /** File mode (type and permission bits). */ + public final int st_mode; + + /** File size in bytes. */ + public final long st_size; + + /** Last modification time in whole seconds (since the Unix epoch). */ + public final long st_mtime; + + /** Nanosecond component of the last modification time. */ + public final long st_mtime_nsec; + + /** + * Creates a new {@code LinuxStructStat} with the given stat field values. + * + * @param st_mode file mode (type + permissions) + * @param st_size file size in bytes + * @param st_mtime modification time, whole seconds + * @param st_mtime_nsec modification time, nanosecond component + */ + public LinuxStructStat(int st_mode, long st_size, long st_mtime, long st_mtime_nsec) { + this.st_mode = st_mode; + this.st_size = st_size; + this.st_mtime = st_mtime; + this.st_mtime_nsec = st_mtime_nsec; + } + + /** + * Converts the stat data into an Eclipse {@link FileInfo}. + * + * @return a {@code FileInfo} populated from the stat fields + */ + public FileInfo toFileInfo() { + FileInfo info = new FileInfo(); + info.setExists(true); + info.setLength(st_size); + + long lastModified = st_mtime * 1_000L; + if (USE_MILLISECOND_RESOLUTION) { + lastModified += st_mtime_nsec / 1_000_000L; + } + info.setLastModified(lastModified); + + if ((st_mode & LinuxFileFlags.S_IFMT) == LinuxFileFlags.S_IFDIR) { + info.setDirectory(true); + } + + // Owner permissions (OWNER_READ and OWNER_WRITE default to true in FileInfo, + // so we only need to explicitly set them to false when the bits are absent) + if ((st_mode & LinuxFileFlags.S_IRUSR) == 0) { + info.setAttribute(EFS.ATTRIBUTE_OWNER_READ, false); + } + if ((st_mode & LinuxFileFlags.S_IWUSR) == 0) { + info.setAttribute(EFS.ATTRIBUTE_OWNER_WRITE, false); + } + if ((st_mode & LinuxFileFlags.S_IXUSR) != 0) { + info.setAttribute(EFS.ATTRIBUTE_OWNER_EXECUTE, true); + } + + // Group permissions + if ((st_mode & LinuxFileFlags.S_IRGRP) != 0) { + info.setAttribute(EFS.ATTRIBUTE_GROUP_READ, true); + } + if ((st_mode & LinuxFileFlags.S_IWGRP) != 0) { + info.setAttribute(EFS.ATTRIBUTE_GROUP_WRITE, true); + } + if ((st_mode & LinuxFileFlags.S_IXGRP) != 0) { + info.setAttribute(EFS.ATTRIBUTE_GROUP_EXECUTE, true); + } + + // Others permissions + if ((st_mode & LinuxFileFlags.S_IROTH) != 0) { + info.setAttribute(EFS.ATTRIBUTE_OTHER_READ, true); + } + if ((st_mode & LinuxFileFlags.S_IWOTH) != 0) { + info.setAttribute(EFS.ATTRIBUTE_OTHER_WRITE, true); + } + if ((st_mode & LinuxFileFlags.S_IXOTH) != 0) { + info.setAttribute(EFS.ATTRIBUTE_OTHER_EXECUTE, true); + } + + return info; + } + +} diff --git a/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/local/LocalFileNativesManager.java b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/local/LocalFileNativesManager.java new file mode 100644 index 00000000000..9f22dd80e6c --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/local/LocalFileNativesManager.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 2025 Eclipse contributors and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Eclipse contributors - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.filesystem.local; + +import java.nio.file.FileSystems; +import java.util.Set; +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.provider.FileInfo; +import org.eclipse.core.internal.filesystem.linux.LinuxFileHandler; +import org.eclipse.core.internal.filesystem.linux.LinuxFileNatives; +import org.eclipse.core.internal.filesystem.local.nio.DefaultHandler; +import org.eclipse.core.internal.filesystem.local.nio.PosixHandler; +import org.eclipse.core.internal.filesystem.local.unix.UnixFileHandler; +import org.eclipse.core.internal.filesystem.local.unix.UnixFileNatives; +import org.eclipse.core.runtime.Platform; + +/** + * Java 25+ override of {@code LocalFileNativesManager}. + * + *When running on Java 25 or later on Linux, this class prefers the + * {@link LinuxFileHandler} backed by the Java FFM API over the legacy + * JNI-based {@code UnixFileHandler}. The FFM implementation calls + * {@code statx(2)}, {@code chmod(2)} and {@code readlink(2)} directly via + * downcall handles without requiring a separately shipped native shared + * library.
+ * + *On all other platforms (macOS, Windows) and when the FFM initialisation + * fails, the selection logic falls back to the same order as the Java 17 + * base-version of this class:
+ *This class is placed in {@code META-INF/versions/25/} of the multi-release + * JAR and is selected automatically by the JVM when the runtime is Java 25+. + * The Java 17 version in the root of the JAR is used on older runtimes.
+ * + * @since 1.11.500 + */ +public class LocalFileNativesManager { + + /** System property that can be used to disable native file operations. */ + public static final String PROPERTY_USE_NATIVES = "eclipse.filesystem.useNatives"; //$NON-NLS-1$ + + /** Default value for {@link #PROPERTY_USE_NATIVES}. */ + public static final boolean PROPERTY_USE_NATIVE_DEFAULT = true; + + private static NativeHandler HANDLER; + + static { + reset(); + } + + /** + * Resets the handler selection to the system default determined by the + * {@value #PROPERTY_USE_NATIVES} system property. + */ + public static void reset() { + setUsingNative(Boolean.parseBoolean( + System.getProperty(PROPERTY_USE_NATIVES, String.valueOf(PROPERTY_USE_NATIVE_DEFAULT)))); + } + + /** + * Attempts to configure the native handler according to the {@code useNatives} + * flag. + * + *On Java 25+ running on Linux the FFM-based {@link LinuxFileHandler} is + * preferred over the legacy JNI handler. On all other configurations the + * selection order matches the Java 17 base-version of this class.
+ * + * @param useNatives {@code true} to try to use a native (FFM or JNI) handler + * @return {@code true} if a native handler is active after this call + */ + public static boolean setUsingNative(boolean useNatives) { + boolean nativesAreUsed; + + if (useNatives && Platform.OS.isLinux() && LinuxFileNatives.isAvailable()) { + // Java 25+ on Linux: use the FFM-based handler. + // No native .so library is needed — libc is accessed directly. + HANDLER = new LinuxFileHandler(); + nativesAreUsed = true; + } else if (useNatives && !Platform.OS.isWindows() && UnixFileNatives.isUsingNatives()) { + // Non-Linux Unix (macOS) or Linux on older Java: fall back to JNI handler. + HANDLER = new UnixFileHandler(); + nativesAreUsed = true; + } else { + nativesAreUsed = false; + Set