From c95e327e2d85c315d141d4447a09c7b2f5d5b77e Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Sat, 12 Sep 2026 14:38:03 +0200 Subject: [PATCH] List Linux directories with their file attributes through the FFM API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PosixHandler lists a directory and then asks java.nio for the attributes of every entry by its full path. On Linux it now reads the directory with opendir and readdir and stats each entry relative to it with statx, through the Foreign Function & Memory API. The layout of struct statx is the same on every architecture, so this needs no compiled library, and the results match the java.nio path exactly, including for broken and self referencing symbolic links. Listing 355984 entries in 36444 directories takes 414 ms against 670 ms for java.nio and 523 ms for the JNI LinuxFileHandler. The bindings are generated by jextract with the script added here, which drops the generated members nothing refers to. statx is bound by hand from the generated descriptor and symbol, because reading errno needs captureCallState, which jextract does not emit, and errno is what tells a missing file from one that cannot be read. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../generateLinuxH.sh | 153 +++++++ .../src-gen/org/linux/LibC$shared.java | 35 ++ .../src-gen/org/linux/LibC.java | 407 ++++++++++++++++++ .../src-gen/org/linux/dirent.java | 96 +++++ .../src-gen/org/linux/statx.java | 173 ++++++++ .../src-gen/org/linux/statx_timestamp.java | 88 ++++ .../local/nio/LinuxDirectoryReader.java | 186 ++++++++ .../filesystem/local/nio/PosixHandler.java | 8 + 8 files changed, 1146 insertions(+) create mode 100755 resources/bundles/org.eclipse.core.filesystem/generateLinuxH.sh create mode 100644 resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC$shared.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/dirent.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx_timestamp.java create mode 100644 resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/LinuxDirectoryReader.java diff --git a/resources/bundles/org.eclipse.core.filesystem/generateLinuxH.sh b/resources/bundles/org.eclipse.core.filesystem/generateLinuxH.sh new file mode 100755 index 00000000000..5f8ba757770 --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/generateLinuxH.sh @@ -0,0 +1,153 @@ +#!/bin/sh +# Regenerates the glibc bindings in src-gen. Needs jextract on the PATH, +# https://jdk.java.net/jextract/, and has to run on Linux. +# +# No --library: that would emit libraryLookup("libc.so"), which cannot be opened +# on glibc, where the library is libc.so.6. Without it the generated lookup falls +# back to the linker's default lookup, which already sees libc. +set -eu +cd "$(dirname "$0")" + +rm -rf src-gen/org/linux +jextract --output src-gen \ + --define-macro _GNU_SOURCE \ + --include-function statx \ + --include-function opendir \ + --include-function readdir \ + --include-function closedir \ + --include-function dirfd \ + --include-function readlinkat \ + --include-struct statx \ + --include-struct statx_timestamp \ + --include-struct dirent \ + --include-constant AT_SYMLINK_NOFOLLOW \ + --include-constant AT_STATX_SYNC_AS_STAT \ + --include-constant STATX_TYPE \ + --include-constant STATX_MODE \ + --include-constant STATX_SIZE \ + --include-constant STATX_MTIME \ + --include-constant ENOENT \ + --include-constant PATH_MAX \ + --include-constant S_IFMT \ + --include-constant S_IFDIR \ + --include-constant S_IFLNK \ + --include-constant S_IRUSR --include-constant S_IWUSR --include-constant S_IXUSR \ + --include-constant S_IRGRP --include-constant S_IWGRP --include-constant S_IXGRP \ + --include-constant S_IROTH --include-constant S_IWOTH --include-constant S_IXOTH \ + --target-package org.linux \ + --header-class-name LibC \ + dirent.h errno.h fcntl.h limits.h sys/stat.h unistd.h + +# jextract cannot select single struct fields or accessors, so drop every generated +# member that neither the sources in src nor another kept member refer to. +find src -name '*.java' | xargs awk -v gen="$(echo src-gen/org/linux/*.java)" ' +function code(line) { + # Drops comments and string literals, which name fields without using them. + if (line ~ /^[[:space:]]*(\/\*|\*|\/\/)/) { + return "" + } + gsub(/"[^"]*"/, "\"\"", line) + return line "\n" +} +function mentions(text, token, rest, i, before, after) { + rest = text + while ((i = index(rest, token)) > 0) { + before = i == 1 ? "" : substr(rest, i - 1, 1) + after = substr(rest, i + length(token), 1) + if (before !~ /[[:alnum:]_$.]/ && after !~ /[[:alnum:]_$]/) { + return 1 + } + rest = substr(rest, i + length(token)) + } + return 0 +} +{ sources = sources code($0) } +END { + n = split(gen, files, " ") + for (f = 1; f <= n; f++) { + cls = files[f]; sub(/.*\//, "", cls); sub(/\.java$/, "", cls) + unit = cls; sub(/\$shared$/, "", unit) + depth = 0; javadoc = 0; complete = 1; phase = "head" + while ((getline line < files[f]) > 0) { + if (phase == "head") { + head[f] = head[f] line "\n" + if (line ~ /^(public )?(final )?class /) { phase = "body" } + continue + } + if (phase == "tail" || (depth == 0 && line == "}")) { + phase = "tail"; tail[f] = tail[f] line "\n" + continue + } + if (m == 0 || complete) { + m++; mfile[m] = f; mcls[m] = cls; munit[m] = unit; complete = 0 + if (line ~ /^[[:space:]]*$/) { blank[m] = 1; complete = 1 } + } + text[m] = text[m] line "\n" + if (blank[m]) { continue } + if (line ~ /^[[:space:]]*\/\*\*/) { javadoc = 1 } + if (javadoc) { + if (line ~ /\*\//) { javadoc = 0 } + continue + } + mcode[m] = mcode[m] code(line) + if (name[m] == "") { + decl = line + if (decl ~ /class [[:alnum:]_$]+/) { + sub(/.*class /, "", decl); sub(/[^[:alnum:]_$].*/, "", decl) + } else { + sub(/[[:space:]]*[=(;].*/, "", decl); sub(/.*[[:space:]]/, "", decl) + } + name[m] = decl + public[m] = line ~ /^[[:space:]]*public / + } + opened = gsub(/\{/, "{", line); closed = gsub(/\}/, "}", line) + depth += opened - closed + if (depth == 0 && line ~ /[;}][[:space:]]*$/) { complete = 1 } + } + close(files[f]) + } + # Keeps what is reachable from the sources, repeating until nothing changes. + do { + changed = 0 + used = sources + for (i = 1; i <= m; i++) { if (keep[i]) { used = used mcode[i] } } + for (i = 1; i <= m; i++) { + if (keep[i] || blank[i]) { continue } + found = name[i] == mcls[i] || mentions(used, mcls[i] "." name[i]) || mentions(used, munit[i] "." name[i]) + for (j = 1; !found && j <= m; j++) { + if (keep[j] && munit[j] == munit[i] && mentions(mcode[j], name[i])) { found = 1 } + } + if (found) { keep[i] = 1; changed = 1 } + } + } while (changed) + for (f = 1; f <= n; f++) { + out = head[f] + for (i = 1; i <= m; i++) { + if (mfile[i] == f && (keep[i] || blank[i])) { out = out text[i] } + } + printf "%s%s", out, tail[f] > files[f] + close(files[f]) + } +}' + +# The bundle's compiler settings report unused imports and every non-NLS string, so drop +# the imports a file does not use and suppress the rest on each top-level class. +used() { + grep -vE '^[[:space:]]*(import |\*|/\*\*|//)' "$2" \ + | grep -qE "(^|[^.[:alnum:]_\$])($1)([^[:alnum:]_\$]|\$)" +} +for f in src-gen/org/linux/*.java; do + cat -s "$f" > "$f.tmp" && mv "$f.tmp" "$f" + for entry in \ + 'java.lang.invoke.*=MethodHandles?|MethodType|VarHandle' \ + 'java.nio.ByteOrder=ByteOrder' \ + 'java.util.*=Arrays|Objects|Optional|List|Map' \ + 'java.util.function.*=Consumer|Function|Supplier|BiFunction|IntFunction' \ + 'java.util.stream.*=Stream|IntStream|Collectors' \ + 'static java.lang.foreign.ValueLayout.*=JAVA_[A-Z_]+|ADDRESS(_UNALIGNED)?|Of[A-Z][a-z]+' \ + 'static java.lang.foreign.MemoryLayout.PathElement.*=groupElement|sequenceElement|dereferenceElement'; do + import=$(printf '%s' "${entry%%=*}" | sed 's/[.*]/\\&/g') + used "${entry#*=}" "$f" || sed -i "\\|^import $import;\$|d" "$f" + done + sed -i 's/^\(public \)\{0,1\}\(final \)\{0,1\}class /@SuppressWarnings("all")\n&/' "$f" +done diff --git a/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC$shared.java b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC$shared.java new file mode 100644 index 00000000000..d37563901dc --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC$shared.java @@ -0,0 +1,35 @@ +// Generated by jextract + +package org.linux; + +import java.lang.foreign.*; +import java.util.*; +import java.util.stream.*; + + +@SuppressWarnings("all") +public class LibC$shared { + + LibC$shared() { + // Should not be called directly + } + + public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char"); + public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short"); + public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int"); + public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long"); + public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*")) + .withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR)); + public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long"); + + static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); + + static void traceDowncall(String name, Object... args) { + String traceArgs = Arrays.stream(args) + .map(Object::toString) + .collect(Collectors.joining(", ")); + System.out.printf("%s(%s)\n", name, traceArgs); + } + +} + diff --git a/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC.java b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC.java new file mode 100644 index 00000000000..06b01a814b6 --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/LibC.java @@ -0,0 +1,407 @@ +// Generated by jextract + +package org.linux; + +import java.lang.invoke.*; +import java.lang.foreign.*; + + +@SuppressWarnings("all") +public class LibC extends LibC$shared { + + LibC() { + // Should not be called directly + } + + static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup() + .or(Linker.nativeLinker().defaultLookup()); + + private static final int PATH_MAX = (int)4096L; + /** + * {@snippet lang=c : + * #define PATH_MAX 4096 + * } + */ + public static int PATH_MAX() { + return PATH_MAX; + } + private static final int ENOENT = (int)2L; + /** + * {@snippet lang=c : + * #define ENOENT 2 + * } + */ + public static int ENOENT() { + return ENOENT; + } + private static final int AT_SYMLINK_NOFOLLOW = (int)256L; + /** + * {@snippet lang=c : + * #define AT_SYMLINK_NOFOLLOW 256 + * } + */ + public static int AT_SYMLINK_NOFOLLOW() { + return AT_SYMLINK_NOFOLLOW; + } + private static final int AT_STATX_SYNC_AS_STAT = (int)0L; + /** + * {@snippet lang=c : + * #define AT_STATX_SYNC_AS_STAT 0 + * } + */ + public static int AT_STATX_SYNC_AS_STAT() { + return AT_STATX_SYNC_AS_STAT; + } + + private static class closedir { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + LibC.C_INT, + LibC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("closedir"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * {@snippet lang=c : + * extern int closedir(DIR *__dirp) + * } + */ + public static int closedir(MemorySegment __dirp) { + var mh$ = closedir.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("closedir", __dirp); + } + return (int)mh$.invokeExact(__dirp); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class opendir { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + LibC.C_POINTER, + LibC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("opendir"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * {@snippet lang=c : + * extern DIR *opendir(const char *__name) + * } + */ + public static MemorySegment opendir(MemorySegment __name) { + var mh$ = opendir.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("opendir", __name); + } + return (MemorySegment)mh$.invokeExact(__name); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class readdir { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + LibC.C_POINTER, + LibC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("readdir"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * {@snippet lang=c : + * extern struct dirent *readdir(DIR *__dirp) + * } + */ + public static MemorySegment readdir(MemorySegment __dirp) { + var mh$ = readdir.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("readdir", __dirp); + } + return (MemorySegment)mh$.invokeExact(__dirp); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class dirfd { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + LibC.C_INT, + LibC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("dirfd"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * {@snippet lang=c : + * extern int dirfd(DIR *__dirp) + * } + */ + public static int dirfd(MemorySegment __dirp) { + var mh$ = dirfd.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("dirfd", __dirp); + } + return (int)mh$.invokeExact(__dirp); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class statx { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + LibC.C_INT, + LibC.C_INT, + LibC.C_POINTER, + LibC.C_INT, + LibC.C_INT, + LibC.C_POINTER + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("statx"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * Function descriptor for: + * {@snippet lang=c : + * int statx(int __dirfd, const char *restrict __path, int __flags, unsigned int __mask, struct statx *restrict __buf) + * } + */ + public static FunctionDescriptor statx$descriptor() { + return statx.DESC; + } + + /** + * Address for: + * {@snippet lang=c : + * int statx(int __dirfd, const char *restrict __path, int __flags, unsigned int __mask, struct statx *restrict __buf) + * } + */ + public static MemorySegment statx$address() { + return statx.ADDR; + } + + /** + * {@snippet lang=c : + * int statx(int __dirfd, const char *restrict __path, int __flags, unsigned int __mask, struct statx *restrict __buf) + * } + */ + public static int statx(int __dirfd, MemorySegment __path, int __flags, int __mask, MemorySegment __buf) { + var mh$ = statx.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("statx", __dirfd, __path, __flags, __mask, __buf); + } + return (int)mh$.invokeExact(__dirfd, __path, __flags, __mask, __buf); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + private static class readlinkat { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + LibC.C_LONG, + LibC.C_INT, + LibC.C_POINTER, + LibC.C_POINTER, + LibC.C_LONG + ); + + public static final MemorySegment ADDR = SYMBOL_LOOKUP.findOrThrow("readlinkat"); + + public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); + } + + /** + * {@snippet lang=c : + * extern ssize_t readlinkat(int __fd, const char *restrict __path, char *restrict __buf, size_t __len) + * } + */ + public static long readlinkat(int __fd, MemorySegment __path, MemorySegment __buf, long __len) { + var mh$ = readlinkat.HANDLE; + try { + if (TRACE_DOWNCALLS) { + traceDowncall("readlinkat", __fd, __path, __buf, __len); + } + return (long)mh$.invokeExact(__fd, __path, __buf, __len); + } catch (Error | RuntimeException ex) { + throw ex; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + private static final int S_IFMT = (int)61440L; + /** + * {@snippet lang=c : + * #define S_IFMT 61440 + * } + */ + public static int S_IFMT() { + return S_IFMT; + } + private static final int S_IFDIR = (int)16384L; + /** + * {@snippet lang=c : + * #define S_IFDIR 16384 + * } + */ + public static int S_IFDIR() { + return S_IFDIR; + } + private static final int S_IFLNK = (int)40960L; + /** + * {@snippet lang=c : + * #define S_IFLNK 40960 + * } + */ + public static int S_IFLNK() { + return S_IFLNK; + } + private static final int S_IRUSR = (int)256L; + /** + * {@snippet lang=c : + * #define S_IRUSR 256 + * } + */ + public static int S_IRUSR() { + return S_IRUSR; + } + private static final int S_IWUSR = (int)128L; + /** + * {@snippet lang=c : + * #define S_IWUSR 128 + * } + */ + public static int S_IWUSR() { + return S_IWUSR; + } + private static final int S_IXUSR = (int)64L; + /** + * {@snippet lang=c : + * #define S_IXUSR 64 + * } + */ + public static int S_IXUSR() { + return S_IXUSR; + } + private static final int S_IRGRP = (int)32L; + /** + * {@snippet lang=c : + * #define S_IRGRP 32 + * } + */ + public static int S_IRGRP() { + return S_IRGRP; + } + private static final int S_IWGRP = (int)16L; + /** + * {@snippet lang=c : + * #define S_IWGRP 16 + * } + */ + public static int S_IWGRP() { + return S_IWGRP; + } + private static final int S_IXGRP = (int)8L; + /** + * {@snippet lang=c : + * #define S_IXGRP 8 + * } + */ + public static int S_IXGRP() { + return S_IXGRP; + } + private static final int S_IROTH = (int)4L; + /** + * {@snippet lang=c : + * #define S_IROTH 4 + * } + */ + public static int S_IROTH() { + return S_IROTH; + } + private static final int S_IWOTH = (int)2L; + /** + * {@snippet lang=c : + * #define S_IWOTH 2 + * } + */ + public static int S_IWOTH() { + return S_IWOTH; + } + private static final int S_IXOTH = (int)1L; + /** + * {@snippet lang=c : + * #define S_IXOTH 1 + * } + */ + public static int S_IXOTH() { + return S_IXOTH; + } + private static final int STATX_TYPE = (int)1L; + /** + * {@snippet lang=c : + * #define STATX_TYPE 1 + * } + */ + public static int STATX_TYPE() { + return STATX_TYPE; + } + private static final int STATX_MODE = (int)2L; + /** + * {@snippet lang=c : + * #define STATX_MODE 2 + * } + */ + public static int STATX_MODE() { + return STATX_MODE; + } + private static final int STATX_MTIME = (int)64L; + /** + * {@snippet lang=c : + * #define STATX_MTIME 64 + * } + */ + public static int STATX_MTIME() { + return STATX_MTIME; + } + private static final int STATX_SIZE = (int)512L; + /** + * {@snippet lang=c : + * #define STATX_SIZE 512 + * } + */ + public static int STATX_SIZE() { + return STATX_SIZE; + } +} + diff --git a/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/dirent.java b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/dirent.java new file mode 100644 index 00000000000..14c99d02fef --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/dirent.java @@ -0,0 +1,96 @@ +// Generated by jextract + +package org.linux; + +import java.lang.invoke.*; +import java.lang.foreign.*; + +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct dirent { + * __ino_t d_ino; + * __off_t d_off; + * unsigned short d_reclen; + * unsigned char d_type; + * char d_name[256]; + * } + * } + */ +@SuppressWarnings("all") +public class dirent { + + dirent() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + LibC.C_LONG.withName("d_ino"), + LibC.C_LONG.withName("d_off"), + LibC.C_SHORT.withName("d_reclen"), + LibC.C_CHAR.withName("d_type"), + MemoryLayout.sequenceLayout(256, LibC.C_CHAR).withName("d_name"), + MemoryLayout.paddingLayout(5) + ).withName("dirent"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final SequenceLayout d_name$LAYOUT = (SequenceLayout)$LAYOUT.select(groupElement("d_name")); + + private static final long d_name$OFFSET = $LAYOUT.byteOffset(groupElement("d_name")); + + /** + * Getter for field: + * {@snippet lang=c : + * char d_name[256] + * } + */ + public static MemorySegment d_name(MemorySegment struct) { + return struct.asSlice(d_name$OFFSET, d_name$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * char d_name[256] + * } + */ + public static void d_name(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, d_name$OFFSET, d_name$LAYOUT.byteSize()); + } + + private static final VarHandle d_name$ELEM_HANDLE = d_name$LAYOUT.varHandle(sequenceElement()); + + /** + * Indexed getter for field: + * {@snippet lang=c : + * char d_name[256] + * } + */ + public static byte d_name(MemorySegment struct, long index0) { + return (byte)d_name$ELEM_HANDLE.get(struct, d_name$OFFSET, index0); + } + + /** + * Indexed setter for field: + * {@snippet lang=c : + * char d_name[256] + * } + */ + public static void d_name(MemorySegment struct, long index0, byte fieldValue) { + d_name$ELEM_HANDLE.set(struct, d_name$OFFSET, index0, fieldValue); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + +} + diff --git a/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx.java b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx.java new file mode 100644 index 00000000000..e537b273f20 --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx.java @@ -0,0 +1,173 @@ +// Generated by jextract + +package org.linux; + +import java.lang.foreign.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct statx { + * __u32 stx_mask; + * __u32 stx_blksize; + * __u64 stx_attributes; + * __u32 stx_nlink; + * __u32 stx_uid; + * __u32 stx_gid; + * __u16 stx_mode; + * __u16 __spare0[1]; + * __u64 stx_ino; + * __u64 stx_size; + * __u64 stx_blocks; + * __u64 stx_attributes_mask; + * struct statx_timestamp stx_atime; + * struct statx_timestamp stx_btime; + * struct statx_timestamp stx_ctime; + * struct statx_timestamp stx_mtime; + * __u32 stx_rdev_major; + * __u32 stx_rdev_minor; + * __u32 stx_dev_major; + * __u32 stx_dev_minor; + * __u64 stx_mnt_id; + * __u32 stx_dio_mem_align; + * __u32 stx_dio_offset_align; + * __u64 stx_subvol; + * __u32 stx_atomic_write_unit_min; + * __u32 stx_atomic_write_unit_max; + * __u32 stx_atomic_write_segments_max; + * __u32 stx_dio_read_offset_align; + * __u32 stx_atomic_write_unit_max_opt; + * __u32 __spare2[1]; + * __u64 __spare3[8]; + * } + * } + */ +@SuppressWarnings("all") +public class statx { + + statx() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + LibC.C_INT.withName("stx_mask"), + LibC.C_INT.withName("stx_blksize"), + LibC.C_LONG_LONG.withName("stx_attributes"), + LibC.C_INT.withName("stx_nlink"), + LibC.C_INT.withName("stx_uid"), + LibC.C_INT.withName("stx_gid"), + LibC.C_SHORT.withName("stx_mode"), + MemoryLayout.sequenceLayout(1, LibC.C_SHORT).withName("__spare0"), + LibC.C_LONG_LONG.withName("stx_ino"), + LibC.C_LONG_LONG.withName("stx_size"), + LibC.C_LONG_LONG.withName("stx_blocks"), + LibC.C_LONG_LONG.withName("stx_attributes_mask"), + statx_timestamp.layout().withName("stx_atime"), + statx_timestamp.layout().withName("stx_btime"), + statx_timestamp.layout().withName("stx_ctime"), + statx_timestamp.layout().withName("stx_mtime"), + LibC.C_INT.withName("stx_rdev_major"), + LibC.C_INT.withName("stx_rdev_minor"), + LibC.C_INT.withName("stx_dev_major"), + LibC.C_INT.withName("stx_dev_minor"), + LibC.C_LONG_LONG.withName("stx_mnt_id"), + LibC.C_INT.withName("stx_dio_mem_align"), + LibC.C_INT.withName("stx_dio_offset_align"), + LibC.C_LONG_LONG.withName("stx_subvol"), + LibC.C_INT.withName("stx_atomic_write_unit_min"), + LibC.C_INT.withName("stx_atomic_write_unit_max"), + LibC.C_INT.withName("stx_atomic_write_segments_max"), + LibC.C_INT.withName("stx_dio_read_offset_align"), + LibC.C_INT.withName("stx_atomic_write_unit_max_opt"), + MemoryLayout.sequenceLayout(1, LibC.C_INT).withName("__spare2"), + MemoryLayout.sequenceLayout(8, LibC.C_LONG_LONG).withName("__spare3") + ).withName("statx"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfShort stx_mode$LAYOUT = (OfShort)$LAYOUT.select(groupElement("stx_mode")); + + private static final long stx_mode$OFFSET = $LAYOUT.byteOffset(groupElement("stx_mode")); + + /** + * Getter for field: + * {@snippet lang=c : + * __u16 stx_mode + * } + */ + public static short stx_mode(MemorySegment struct) { + return struct.get(stx_mode$LAYOUT, stx_mode$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * __u16 stx_mode + * } + */ + public static void stx_mode(MemorySegment struct, short fieldValue) { + struct.set(stx_mode$LAYOUT, stx_mode$OFFSET, fieldValue); + } + + private static final OfLong stx_size$LAYOUT = (OfLong)$LAYOUT.select(groupElement("stx_size")); + + private static final long stx_size$OFFSET = $LAYOUT.byteOffset(groupElement("stx_size")); + + /** + * Getter for field: + * {@snippet lang=c : + * __u64 stx_size + * } + */ + public static long stx_size(MemorySegment struct) { + return struct.get(stx_size$LAYOUT, stx_size$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * __u64 stx_size + * } + */ + public static void stx_size(MemorySegment struct, long fieldValue) { + struct.set(stx_size$LAYOUT, stx_size$OFFSET, fieldValue); + } + + private static final GroupLayout stx_mtime$LAYOUT = (GroupLayout)$LAYOUT.select(groupElement("stx_mtime")); + + private static final long stx_mtime$OFFSET = $LAYOUT.byteOffset(groupElement("stx_mtime")); + + /** + * Getter for field: + * {@snippet lang=c : + * struct statx_timestamp stx_mtime + * } + */ + public static MemorySegment stx_mtime(MemorySegment struct) { + return struct.asSlice(stx_mtime$OFFSET, stx_mtime$LAYOUT.byteSize()); + } + + /** + * Setter for field: + * {@snippet lang=c : + * struct statx_timestamp stx_mtime + * } + */ + public static void stx_mtime(MemorySegment struct, MemorySegment fieldValue) { + MemorySegment.copy(fieldValue, 0L, struct, stx_mtime$OFFSET, stx_mtime$LAYOUT.byteSize()); + } + + /** + * The size (in bytes) of this struct + */ + public static long sizeof() { return layout().byteSize(); } + +} + diff --git a/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx_timestamp.java b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx_timestamp.java new file mode 100644 index 00000000000..9a5d5f7f7c8 --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src-gen/org/linux/statx_timestamp.java @@ -0,0 +1,88 @@ +// Generated by jextract + +package org.linux; + +import java.lang.foreign.*; + +import static java.lang.foreign.ValueLayout.*; +import static java.lang.foreign.MemoryLayout.PathElement.*; + +/** + * {@snippet lang=c : + * struct statx_timestamp { + * __s64 tv_sec; + * __u32 tv_nsec; + * __s32 __reserved; + * } + * } + */ +@SuppressWarnings("all") +public class statx_timestamp { + + statx_timestamp() { + // Should not be called directly + } + + private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( + LibC.C_LONG_LONG.withName("tv_sec"), + LibC.C_INT.withName("tv_nsec"), + LibC.C_INT.withName("__reserved") + ).withName("statx_timestamp"); + + /** + * The layout of this struct + */ + public static final GroupLayout layout() { + return $LAYOUT; + } + + private static final OfLong tv_sec$LAYOUT = (OfLong)$LAYOUT.select(groupElement("tv_sec")); + + private static final long tv_sec$OFFSET = $LAYOUT.byteOffset(groupElement("tv_sec")); + + /** + * Getter for field: + * {@snippet lang=c : + * __s64 tv_sec + * } + */ + public static long tv_sec(MemorySegment struct) { + return struct.get(tv_sec$LAYOUT, tv_sec$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * __s64 tv_sec + * } + */ + public static void tv_sec(MemorySegment struct, long fieldValue) { + struct.set(tv_sec$LAYOUT, tv_sec$OFFSET, fieldValue); + } + + private static final OfInt tv_nsec$LAYOUT = (OfInt)$LAYOUT.select(groupElement("tv_nsec")); + + private static final long tv_nsec$OFFSET = $LAYOUT.byteOffset(groupElement("tv_nsec")); + + /** + * Getter for field: + * {@snippet lang=c : + * __u32 tv_nsec + * } + */ + public static int tv_nsec(MemorySegment struct) { + return struct.get(tv_nsec$LAYOUT, tv_nsec$OFFSET); + } + + /** + * Setter for field: + * {@snippet lang=c : + * __u32 tv_nsec + * } + */ + public static void tv_nsec(MemorySegment struct, int fieldValue) { + struct.set(tv_nsec$LAYOUT, tv_nsec$OFFSET, fieldValue); + } + +} + diff --git a/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/LinuxDirectoryReader.java b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/LinuxDirectoryReader.java new file mode 100644 index 00000000000..8b5804186b0 --- /dev/null +++ b/resources/bundles/org.eclipse.core.filesystem/src/org/eclipse/core/internal/filesystem/local/nio/LinuxDirectoryReader.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2026 vogella GmbH 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: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.filesystem.local.nio; + +import java.lang.foreign.Arena; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileInfo; +import org.eclipse.core.filesystem.provider.FileInfo; +import org.eclipse.core.internal.filesystem.local.Convert; +import org.eclipse.core.runtime.Platform; +import org.linux.LibC; +import org.linux.dirent; +import org.linux.statx; +import org.linux.statx_timestamp; + +/** + * Lists a directory together with the attributes of its entries on Linux, with + * one {@code readdir} and one {@code statx} per entry through the Foreign + * Function & Memory API instead of a {@code java.nio} lookup per file. + */ +final class LinuxDirectoryReader { + + private static final IFileInfo[] NO_INFOS = {}; + + private static final Charset PLATFORM_CHARSET = Platform.getSystemCharset(); + + private static final MemoryLayout ERRNO_LAYOUT = Linker.Option.captureStateLayout(); + private static final VarHandle ERRNO = ERRNO_LAYOUT + .varHandle(MemoryLayout.PathElement.groupElement("errno")); //$NON-NLS-1$ + + // jextract emits no captureCallState, and errno is what tells a missing file from an + // unreadable one. + private static final MethodHandle STATX = Platform.OS.isLinux() ? statxWithErrno() : null; + + private LinuxDirectoryReader() { + } + + private static MethodHandle statxWithErrno() { + try { + return Linker.nativeLinker().downcallHandle(LibC.statx$address(), LibC.statx$descriptor(), + Linker.Option.captureCallState("errno")); //$NON-NLS-1$ + } catch (NoSuchElementException | IllegalCallerException | UnsupportedOperationException + | ExceptionInInitializerError | NoClassDefFoundError e) { + // statx needs glibc 2.28, and native access may be denied outright + return null; + } + } + + static boolean isAvailable() { + return STATX != null; + } + + /** + * The native buffers these calls need, held per thread and reused, so that a + * listing allocates nothing per entry beyond its {@link FileInfo}. + */ + private static final class Scratch { + private static final ThreadLocal CURRENT = ThreadLocal.withInitial(Scratch::new); + + private final Arena arena = Arena.ofAuto(); + final MemorySegment errno = arena.allocate(ERRNO_LAYOUT); + final MemorySegment stat = arena.allocate(statx.sizeof()); + final MemorySegment linkTarget = arena.allocate(LibC.PATH_MAX()); + private final MemorySegment path = arena.allocate(LibC.PATH_MAX() + 1); + + /** The file name as a NUL terminated C string. */ + MemorySegment path(String fileName) { + byte[] bytes = Convert.toPlatformBytes(fileName); + MemorySegment target = bytes.length < path.byteSize() ? path : arena.allocate(bytes.length + 1); + MemorySegment.copy(bytes, 0, target, ValueLayout.JAVA_BYTE, 0, bytes.length); + target.set(ValueLayout.JAVA_BYTE, bytes.length, (byte) 0); + return target; + } + } + + static IFileInfo[] listDirectoryAndGetFileInfos(String fileName) { + Scratch scratch = Scratch.CURRENT.get(); + MemorySegment dir = LibC.opendir(scratch.path(fileName)); + if (dir.address() == 0) { + return NO_INFOS; + } + try { + int dirFd = LibC.dirfd(dir); + List infos = new ArrayList<>(); + while (true) { + MemorySegment entry = LibC.readdir(dir); + if (entry.address() == 0) { + return infos.toArray(IFileInfo[]::new); + } + // d_name is NUL terminated inside the struct, so bounding the entry by its + // declared size never cuts the name short. + MemorySegment name = dirent.d_name(entry.reinterpret(dirent.sizeof())); + String nameString = name.getString(0, PLATFORM_CHARSET); + if (!".".equals(nameString) && !"..".equals(nameString)) { //$NON-NLS-1$ //$NON-NLS-2$ + infos.add(fetchFileInfo(scratch, dirFd, name, nameString)); + } + } + } finally { + LibC.closedir(dir); + } + } + + /** + * Mirrors {@link PosixHandler#fetchFileInfo(String)}: a symbolic link carries + * the attributes of its target, a missing file is no error, and any other + * failure is an I/O error. + */ + private static FileInfo fetchFileInfo(Scratch scratch, int dirFd, MemorySegment path, String name) { + FileInfo info = new FileInfo(name); + int errno = statx(scratch, dirFd, path, false); + if (errno == 0 && (mode(scratch.stat) & LibC.S_IFMT()) == LibC.S_IFLNK()) { + info.setAttribute(EFS.ATTRIBUTE_SYMLINK, true); + long length = LibC.readlinkat(dirFd, path, scratch.linkTarget, scratch.linkTarget.byteSize()); + if (length > 0) { + byte[] target = scratch.linkTarget.asSlice(0, length).toArray(ValueLayout.JAVA_BYTE); + info.setStringAttribute(EFS.ATTRIBUTE_LINK_TARGET, new String(target, PLATFORM_CHARSET)); + } + errno = statx(scratch, dirFd, path, true); + } + if (errno == LibC.ENOENT()) { + return info; + } + if (errno != 0) { + info.setError(IFileInfo.IO_ERROR); + return info; + } + MemorySegment stat = scratch.stat; + int mode = mode(stat); + MemorySegment modified = statx.stx_mtime(stat); + info.setExists(true); + info.setLastModified(statx_timestamp.tv_sec(modified) * 1_000 + + Integer.toUnsignedLong(statx_timestamp.tv_nsec(modified)) / 1_000_000); + info.setLength(statx.stx_size(stat)); + info.setDirectory((mode & LibC.S_IFMT()) == LibC.S_IFDIR()); + info.setAttribute(EFS.ATTRIBUTE_OWNER_READ, (mode & LibC.S_IRUSR()) != 0); + info.setAttribute(EFS.ATTRIBUTE_OWNER_WRITE, (mode & LibC.S_IWUSR()) != 0); + info.setAttribute(EFS.ATTRIBUTE_OWNER_EXECUTE, (mode & LibC.S_IXUSR()) != 0); + info.setAttribute(EFS.ATTRIBUTE_GROUP_READ, (mode & LibC.S_IRGRP()) != 0); + info.setAttribute(EFS.ATTRIBUTE_GROUP_WRITE, (mode & LibC.S_IWGRP()) != 0); + info.setAttribute(EFS.ATTRIBUTE_GROUP_EXECUTE, (mode & LibC.S_IXGRP()) != 0); + info.setAttribute(EFS.ATTRIBUTE_OTHER_READ, (mode & LibC.S_IROTH()) != 0); + info.setAttribute(EFS.ATTRIBUTE_OTHER_WRITE, (mode & LibC.S_IWOTH()) != 0); + info.setAttribute(EFS.ATTRIBUTE_OTHER_EXECUTE, (mode & LibC.S_IXOTH()) != 0); + return info; + } + + /** Fills {@code scratch.stat} and returns 0, or the errno of the failure. */ + private static int statx(Scratch scratch, int dirFd, MemorySegment path, boolean followLinks) { + int flags = LibC.AT_STATX_SYNC_AS_STAT() | (followLinks ? 0 : LibC.AT_SYMLINK_NOFOLLOW()); + int mask = LibC.STATX_TYPE() | LibC.STATX_MODE() | LibC.STATX_SIZE() | LibC.STATX_MTIME(); + int result; + try { + result = (int) STATX.invokeExact(scratch.errno, dirFd, path, flags, mask, scratch.stat); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new IllegalStateException(e); + } + return result == 0 ? 0 : (int) ERRNO.get(scratch.errno, 0L); + } + + private static int mode(MemorySegment stat) { + return Short.toUnsignedInt(statx.stx_mode(stat)); + } +} 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 726f11c20b7..191ec822168 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 @@ -93,6 +93,14 @@ public int getSupportedAttributes() { return ATTRIBUTES; } + @Override + public IFileInfo[] listDirectoryAndGetFileInfos(String fileName) { + if (LinuxDirectoryReader.isAvailable()) { + return LinuxDirectoryReader.listDirectoryAndGetFileInfos(fileName); + } + return super.listDirectoryAndGetFileInfos(fileName); + } + @Override public boolean putFileInfo(String fileName, IFileInfo info) { Path path = Paths.get(fileName);