From 0dc59778574460fc13af7ac157b75f5283b4904a Mon Sep 17 00:00:00 2001 From: Andrey Loskutov Date: Mon, 29 Jun 2026 16:26:36 +0200 Subject: [PATCH] [Draft] naive attempt to move Linux native code to FFM API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOT TESTED, as PDE always loads classes from "bin" directory with current state, so debugging is not possible. - MR project configuration created via Copilot, but it was broken and required manual changes. - Mac parts are intentionally left over for now to simplify FFM code - `org.eclipse.core.internal.filesystem.linux` package contains corresponding Mac-free Java 25 code - `LocalFileNativesManager` from `src25` is supposed to be entry point to Java 25 code and "overrides" `LocalFileNativesManager` from `src` on Java 25 - `LinuxFileNatives` contains FFM code generated by Copilot from unixfile.c - The old code / library is still there and should work without Java 25 code or if Java 25 is not able to load FFM version. Below is the summary of the refactoring generated by Copilot This refactoring introduces a Java 25 Foreign Function & Memory (FFM) API based implementation of the native file operations for Linux, replacing the JNI/C native library path for Java 25+ runtimes. The existing JNI implementation remainsunchanged and is still used on macOS and on older Java runtimes. --- The original code used JNI (Java Native Interface) to call C functions for file attribute operations (stat, chmod, readlink). This required: - A compiled native shared library (`libunixfile_1_0_0.so`) to be present - Per-architecture fragment bundles (x86_64, aarch64, ppc64le, loongarch64) - C code with `#ifdef MACOSX` / `#ifndef MACOSX` guards mixed throughout - Separate fragment project per Linux architecture The Java 25 FFM API (finalized since Java 22, `java.lang.foreign`) allows direct calls into the C standard library without any native shared library, and with no per-architecture C code required. --- The classic `stat` struct has **architecture-dependent layout** due to different field ordering and padding on x86_64, aarch64, ppc64le, s390x, riscv64, etc. Correctly reproducing this in Java requires either hard-coded per-arch layouts or detection at runtime. The `statx(2)` syscall (available since Linux 4.11) uses a **fixed, ABI-stable struct layout** that is identical across all Linux architectures. The `statx` struct is defined in `` and will never change its existing field offsets. This makes `statx` the ideal choice for a portable, architecture-independent FFM implementation on Linux. Key `statx` struct field offsets used (stable since Linux 4.11): | Field | Offset | Type | Meaning | |-------|--------|------|---------| | `stx_mode` | 28 | `__u16` | File type + permission bits | | `stx_size` | 40 | `__u64` | File size in bytes | | `stx_mtime.tv_sec` | 112 | `__s64` | Modification time (seconds) | | `stx_mtime.tv_nsec` | 120 | `__u32` | Modification time (nanoseconds) | The old `UnixFileNatives.java` and `unixfile.c` served both Linux and macOS. Mac-specific code paths included: - **`chflags(2)`**: Sets immutable flag (`UF_IMMUTABLE`, `SF_IMMUTABLE`). Only available on macOS (BSD-style file flags). Not a Linux syscall. - **`tounicode()`**: Converts filenames using CoreServices CFString for macOS NFD Unicode normalization. Not relevant on Linux. - **`libattr` bitmask**: Returned `UNICODE_SUPPORTED | CHFLAGS_SUPPORTED` on macOS, `0` on Linux — the whole mechanism existed only to support Mac features. - **`st_flags`** in `StructStat`: The macOS-only `chflags` flags field. Not present in the Linux stat structures. All of these are completely absent from the new `LinuxFile*` classes. The new Linux implementation also does **not** support `ATTRIBUTE_IMMUTABLE` (which maps to BSD chflags) because Linux does not have this mechanism. The old code had a `UNICODE_SUPPORTED` flag and a `tounicode()` path specifically to handle macOS NFD filename normalization via CoreServices. On Linux, all modern filesystems use UTF-8 and filenames are passed byte-for-byte. The new FFM code simply uses `StandardCharsets.UTF_8` for encoding/decoding filenames, with no normalization step needed. Because the new implementation uses FFM downcall handles targeting libc (`statx`, `chmod`, `readlink`), no separately compiled `.so` file is needed. The standard C library is always available on Linux and is accessed directly by the JVM through the `Linker.nativeLinker().defaultLookup()`. This means the Linux fragment bundles (`org.eclipse.core.filesystem.linux.x86_64`, etc.) are only needed for Java < 25 runtimes. On Java 25+, the FFM path activates automatically and the native library is not loaded. The FFM API provides `Linker.Option.captureCallState("errno")` which captures the C `errno` value immediately after the native call (before any Java code runs that could modify the thread-local errno). This is safer than the JNI approach which called a separate `errno()` native function later. --- All placed under `src25/org/eclipse/core/internal/filesystem/`: | File | Description | |------|-------------| | `linux/LinuxFileFlags.java` | Hard-coded Linux file mode constants (POSIX, same on all Linux archs). No `UF_IMMUTABLE` / `SF_IMMUTABLE` (Mac-only). | | `linux/LinuxStructStat.java` | Plain Java data class mirroring the fields from `statx` that are relevant for EFS. No `st_flags` (Mac-only). Converts to `FileInfo`. | | `linux/LinuxFileNatives.java` | FFM-based implementation of `fetchFileInfo` and `putFileInfo`. Uses `statx(2)`, `chmod(2)`, `readlink(2)` via downcall handles. | | `linux/LinuxFileHandler.java` | Extends `NativeHandler`; delegates to `LinuxFileNatives`. Used as the active handler on Java 25+ / Linux. | | `local/LocalFileNativesManager.java` | Java 25 multi-release override. Selects `LinuxFileHandler` on Java 25+ Linux, falls back to `UnixFileHandler` (JNI) on macOS or older Java, then to POSIX NIO-2, Win32, or Default handler. | | File | Change | |------|--------| | `META-INF/MANIFEST.MF` | Added `Multi-Release: true`; exported the new `org.eclipse.core.internal.filesystem.linux` package. | | `build.properties` | Added `source.META-INF/versions/25/ = src25/` and `output.META-INF/versions/25/ = bin25/` to instruct Tycho to compile `src25/` into `META-INF/versions/25/` of the JAR. | | `.classpath` | Added `src25` as a source folder with output `bin25` for Eclipse IDE support. | All existing files remain untouched: - `src/…/unix/UnixFileHandler.java` - `src/…/unix/UnixFileFlags.java` - `src/…/unix/UnixFileNatives.java` - `src/…/unix/StructStat.java` - `src/…/local/LocalFileNativesManager.java` (the Java 17 base version) - `natives/unix/unixfile.c` and `unixfile.h` --- ``` org.eclipse.core.filesystem.jar ├── org/eclipse/core/internal/filesystem/local/LocalFileNativesManager.class ← Java 17 version ├── org/eclipse/core/internal/filesystem/local/unix/UnixFileHandler.class ├── org/eclipse/core/internal/filesystem/local/unix/UnixFileNatives.class └── META-INF/ ├── MANIFEST.MF (Multi-Release: true) └── versions/ └── 25/ └── org/eclipse/core/internal/filesystem/ ├── local/LocalFileNativesManager.class ← Java 25 override └── linux/ ├── LinuxFileFlags.class ├── LinuxStructStat.class ├── LinuxFileNatives.class └── LinuxFileHandler.class ``` When the JVM is Java 25+, it automatically uses the `META-INF/versions/25/` version of `LocalFileNativesManager`, which in turn selects `LinuxFileHandler` on Linux. On older JVMs the root version is used and behaviour is unchanged. --- | Environment | Handler selected | |-------------|-----------------| | Java 25+ on Linux, FFM init successful | `LinuxFileHandler` (FFM, no native .so needed) | | Java 25+ on Linux, FFM init failed | `UnixFileHandler` (JNI, requires libunixfile) | | Java 25+ on macOS | `UnixFileHandler` (JNI) | | Java 17–24 on Linux | `UnixFileHandler` (JNI) | | Any version, no native lib, POSIX NIO-2 | `PosixHandler` | | Any version, no native lib, DOS NIO-2 | `Win32Handler` | | Any version, fallback | `DefaultHandler` | --- --- .../org.eclipse.core.filesystem/.classpath | 13 +- .../org.eclipse.core.filesystem/.gitignore | 1 + .../META-INF/MANIFEST.MF | 3 + .../META-INF/versions/25/OSGI-INF/MANIFEST.MF | 2 + .../build.properties | 2 + .../filesystem/linux/LinuxFileFlags.java | 73 ++++ .../filesystem/linux/LinuxFileHandler.java | 53 +++ .../filesystem/linux/LinuxFileNatives.java | 318 ++++++++++++++++++ .../filesystem/linux/LinuxStructStat.java | 118 +++++++ .../local/LocalFileNativesManager.java | 145 ++++++++ 10 files changed, 726 insertions(+), 2 deletions(-) create mode 100644 resources/bundles/org.eclipse.core.filesystem/.gitignore create mode 100644 resources/bundles/org.eclipse.core.filesystem/META-INF/versions/25/OSGI-INF/MANIFEST.MF create mode 100644 resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileFlags.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileHandler.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileNatives.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxStructStat.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/local/LocalFileNativesManager.java 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 @@ - + + + + + - + + + + + + diff --git a/resources/bundles/org.eclipse.core.filesystem/.gitignore b/resources/bundles/org.eclipse.core.filesystem/.gitignore new file mode 100644 index 00000000000..72876f038fe --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/.gitignore @@ -0,0 +1 @@ +/bin25/ 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..11afd81f7a3 100644 --- a/resources/bundles/org.eclipse.core.filesystem/META-INF/MANIFEST.MF +++ b/resources/bundles/org.eclipse.core.filesystem/META-INF/MANIFEST.MF @@ -8,11 +8,14 @@ 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", org.eclipse.core.filesystem.provider;uses:="org.eclipse.core.filesystem,org.eclipse.core.runtime", org.eclipse.core.internal.filesystem;x-internal:=true, + org.eclipse.core.internal.filesystem.linux;x-internal:=true, org.eclipse.core.internal.filesystem.local;x-internal:=true, org.eclipse.core.internal.filesystem.local.unix;x-internal:=true Bundle-Vendor: %providerName Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Automatic-Module-Name: org.eclipse.core.filesystem +Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=17))" +Multi-Release: true Import-Package: com.sun.jna;version="[5.14.0,6.0.0)", com.sun.jna.platform.win32;version="[5.14.0,6.0.0)" diff --git a/resources/bundles/org.eclipse.core.filesystem/META-INF/versions/25/OSGI-INF/MANIFEST.MF b/resources/bundles/org.eclipse.core.filesystem/META-INF/versions/25/OSGI-INF/MANIFEST.MF new file mode 100644 index 00000000000..83a03b4c8cd --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/META-INF/versions/25/OSGI-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=25))" \ No newline at end of file diff --git a/resources/bundles/org.eclipse.core.filesystem/build.properties b/resources/bundles/org.eclipse.core.filesystem/build.properties index a8d6005911d..0693c427ebd 100644 --- a/resources/bundles/org.eclipse.core.filesystem/build.properties +++ b/resources/bundles/org.eclipse.core.filesystem/build.properties @@ -13,6 +13,8 @@ ############################################################################### source.. = src/ output.. = bin/ +source.META-INF/versions/25/ = src25/ +output.META-INF/versions/25/ = bin25/ bin.includes = META-INF/,\ .,\ plugin.xml,\ diff --git a/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileFlags.java b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileFlags.java new file mode 100644 index 00000000000..3a3fff02d3c --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileFlags.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * 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; + +/** + * Linux-specific file mode flags. + * + *

These are standard POSIX constants as defined in {@code } and are + * identical across all Linux architectures. No Mac OS X specific flags + * (such as {@code UF_IMMUTABLE} or {@code SF_IMMUTABLE}) are present here since + * they are not supported on Linux.

+ * + * @since 1.11.500 + */ +public final class LinuxFileFlags { + + /** + * Maximum number of characters in a file path including the null terminator. + */ + public static final int PATH_MAX = 4096; + + /** Bitmask for the file type bitfields in {@code st_mode}. */ + public static final int S_IFMT = 0xF000; + + /** File type: symbolic link. */ + public static final int S_IFLNK = 0xA000; + + /** File type: directory. */ + public static final int S_IFDIR = 0x4000; + + /** Owner has read permission. */ + public static final int S_IRUSR = 0x0100; + + /** Owner has write permission. */ + public static final int S_IWUSR = 0x0080; + + /** Owner has execute permission. */ + public static final int S_IXUSR = 0x0040; + + /** Group has read permission. */ + public static final int S_IRGRP = 0x0020; + + /** Group has write permission. */ + public static final int S_IWGRP = 0x0010; + + /** Group has execute permission. */ + public static final int S_IXGRP = 0x0008; + + /** Others have read permission. */ + public static final int S_IROTH = 0x0004; + + /** Others have write permission. */ + public static final int S_IWOTH = 0x0002; + + /** Others have execute permission. */ + public static final int S_IXOTH = 0x0001; + + private LinuxFileFlags() { + // not instantiable + } + +} diff --git a/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileHandler.java b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileHandler.java new file mode 100644 index 00000000000..d42fd558ac7 --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src25/org/eclipse/core/internal/filesystem/linux/LinuxFileHandler.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * 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.IFileInfo; +import org.eclipse.core.filesystem.provider.FileInfo; +import org.eclipse.core.internal.filesystem.local.NativeHandler; + +/** + * A {@link NativeHandler} for Linux that uses the Java 25 Foreign Function & + * Memory (FFM) API instead of JNI to access native file operations. + * + *

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 in and is ABI-stable + // across all Linux architectures since Linux 4.11. The offsets below are + // the same on x86_64, aarch64, riscv64, ppc64le, s390x and every other + // Linux architecture — there is no per-arch padding variation like in the + // classic stat struct. + // + // Relevant fields used by this implementation: + // + // offset 28: __u16 stx_mode (file type + permission bits) + // offset 40: __u64 stx_size (file size in bytes) + // offset 112: __s64 stx_mtime.tv_sec (modification time, seconds) + // offset 120: __u32 stx_mtime.tv_nsec (modification time, nanoseconds) + + private static final long STATX_STRUCT_SIZE = 256L; + private static final long STATX_MODE_OFFSET = 28L; + private static final long STATX_SIZE_OFFSET = 40L; + private static final long STATX_MTIME_SEC_OFFSET = 112L; + private static final long STATX_MTIME_NSEC_OFFSET = 120L; + + // ---------- FFM handles and state ------------------------------------------ + + private static final StructLayout CAPTURED_STATE_LAYOUT; + private static final VarHandle ERRNO_VH; + private static final MethodHandle STATX_MH; + private static final MethodHandle CHMOD_MH; + private static final MethodHandle READLINK_MH; + private static final boolean AVAILABLE; + + static { + StructLayout capturedStateLayout = null; + VarHandle errnoVH = null; + MethodHandle statxMH = null; + MethodHandle chmodMH = null; + MethodHandle readlinkMH = null; + boolean available = false; + + try { + Linker linker = Linker.nativeLinker(); + SymbolLookup lookup = linker.defaultLookup(); + Linker.Option captureErrno = Linker.Option.captureCallState("errno"); //$NON-NLS-1$ + + capturedStateLayout = Linker.Option.captureStateLayout(); + errnoVH = capturedStateLayout.varHandle( + MemoryLayout.PathElement.groupElement("errno")); //$NON-NLS-1$ + + // int statx(int dirfd, const char *pathname, int flags, + // unsigned int mask, struct statx *statxbuf) + statxMH = linker.downcallHandle( + lookup.find("statx").orElseThrow(), //$NON-NLS-1$ + FunctionDescriptor.of( + ValueLayout.JAVA_INT, // return: int + ValueLayout.JAVA_INT, // dirfd + ValueLayout.ADDRESS, // pathname (const char *) + ValueLayout.JAVA_INT, // flags + ValueLayout.JAVA_INT, // mask (unsigned int) + ValueLayout.ADDRESS), // statxbuf (struct statx *) + captureErrno); + + // int chmod(const char *pathname, mode_t mode) + // mode_t is unsigned int (32-bit) on all Linux architectures + chmodMH = linker.downcallHandle( + lookup.find("chmod").orElseThrow(), //$NON-NLS-1$ + FunctionDescriptor.of( + ValueLayout.JAVA_INT, // return: int + ValueLayout.ADDRESS, // pathname (const char *) + ValueLayout.JAVA_INT), // mode (mode_t) + captureErrno); + + // ssize_t readlink(const char *pathname, char *buf, size_t bufsiz) + // ssize_t and size_t are 64-bit on all 64-bit Linux architectures + readlinkMH = linker.downcallHandle( + lookup.find("readlink").orElseThrow(), //$NON-NLS-1$ + FunctionDescriptor.of( + ValueLayout.JAVA_LONG, // return: ssize_t + ValueLayout.ADDRESS, // pathname (const char *) + ValueLayout.ADDRESS, // buf (char *) + ValueLayout.JAVA_LONG), // bufsiz (size_t) + captureErrno); + + available = true; + } catch (Exception e) { + // FFM API is not available or required libc symbols could not be found. + // isAvailable() returns false and all callers fall back gracefully. + } + + AVAILABLE = available; + CAPTURED_STATE_LAYOUT = capturedStateLayout; + ERRNO_VH = errnoVH; + STATX_MH = statxMH; + CHMOD_MH = chmodMH; + READLINK_MH = readlinkMH; + } + + // ---------- Public API ----------------------------------------------------- + + /** + * Returns {@code true} if the FFM downcall handles were initialised + * successfully and this class can be used. + * + * @return {@code true} when FFM-based native file operations are available + */ + public static boolean isAvailable() { + return AVAILABLE; + } + + /** + * Returns the bitmask of EFS file attributes supported by this implementation. + * + * @return supported EFS attribute bitmask, or {@code -1} if FFM is not available + */ + public static int getSupportedAttributes() { + if (!AVAILABLE) { + return -1; + } + // No ATTRIBUTE_IMMUTABLE: Linux does not support the BSD chflags mechanism. + return EFS.ATTRIBUTE_READ_ONLY | EFS.ATTRIBUTE_EXECUTABLE + | EFS.ATTRIBUTE_SYMLINK | EFS.ATTRIBUTE_LINK_TARGET + | EFS.ATTRIBUTE_OWNER_READ | EFS.ATTRIBUTE_OWNER_WRITE | EFS.ATTRIBUTE_OWNER_EXECUTE + | EFS.ATTRIBUTE_GROUP_READ | EFS.ATTRIBUTE_GROUP_WRITE | EFS.ATTRIBUTE_GROUP_EXECUTE + | EFS.ATTRIBUTE_OTHER_READ | EFS.ATTRIBUTE_OTHER_WRITE | EFS.ATTRIBUTE_OTHER_EXECUTE; + } + + /** + * Fetches file information for the given path. + * + *

Follows 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:

+ *
    + *
  1. JNI {@code UnixFileHandler} (non-Windows, when native library is present)
  2. + *
  3. POSIX NIO/2 {@code PosixHandler}
  4. + *
  5. DOS NIO/2 {@code Win32Handler}
  6. + *
  7. {@code DefaultHandler}
  8. + *
+ * + *

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 views = FileSystems.getDefault().supportedFileAttributeViews(); + if (views.contains("posix")) { //$NON-NLS-1$ + HANDLER = new PosixHandler(); + } else if (views.contains("dos")) { //$NON-NLS-1$ + HANDLER = new Win32Handler(); + } else { + HANDLER = new DefaultHandler(); + } + } + return nativesAreUsed; + } + + /** + * Returns the bitmask of EFS attributes supported by the active handler. + * + * @return EFS attribute bitmask + */ + public static int getSupportedAttributes() { + return HANDLER.getSupportedAttributes(); + } + + /** + * Fetches file information for the given file path. + * + * @param fileName absolute path of the file + * @return {@link FileInfo} populated with the file's attributes + */ + public static FileInfo fetchFileInfo(String fileName) { + return HANDLER.fetchFileInfo(fileName); + } + + /** + * Writes file information (permissions) for the given file path. + * + * @param fileName absolute path of the file + * @param info the desired file attributes + * @param options option flags (currently unused) + * @return {@code true} if the operation succeeded + */ + public static boolean putFileInfo(String fileName, IFileInfo info, int options) { + return HANDLER.putFileInfo(fileName, info, options); + } + +}