diff --git a/jfuse-api/src/main/java/org/cryptomator/jfuse/api/FuseConnInfo.java b/jfuse-api/src/main/java/org/cryptomator/jfuse/api/FuseConnInfo.java index e9b2718f..90765b3b 100644 --- a/jfuse-api/src/main/java/org/cryptomator/jfuse/api/FuseConnInfo.java +++ b/jfuse-api/src/main/java/org/cryptomator/jfuse/api/FuseConnInfo.java @@ -323,6 +323,7 @@ public interface FuseConnInfo { * Capability flags that the kernel supports (read-only) * * @return {@code capable} value + * @apiNote Since libFUSE 3.17, this field is deprecated left over for ABI compatibility, use capable_ext */ int capable(); @@ -333,6 +334,8 @@ public interface FuseConnInfo { * reasonable default values before calling the init() handler. * * @return {@code want} value + * @apiNote Since libFUSE 3.17, this field is deprecated left over for ABI compatibility. + * Use want_ext with the helper functions fuse_set_feature_flag() / fuse_unset_feature_flag() */ int want(); @@ -340,6 +343,8 @@ public interface FuseConnInfo { * Sets the {@link #want()} value. * * @param wanted {@code want} value + * @apiNote Since libFUSE 3.17, this field is deprecated left over for ABI compatibility. + * Use want_ext with the helper functions fuse_set_feature_flag() / fuse_unset_feature_flag() */ void setWant(int wanted); @@ -499,4 +504,87 @@ default int asyncRead() { default void setAsyncRead(int asyncRead) { //no-op } + + /** + * Extended capability flags that the kernel supports (read-only) + * This field provides full 64-bit capability support. + *

+ * If the version of the loaded FUSE library is below 3.17, this method does nothing. + * + * @implSpec The default implementation always returns 0. + * @since libFUSE 3.17 + */ + default long capableExt() { + return 0; + } + + /** + * Extended capability flags that the filesystem wants to enable. + * This field provides full 64-bit capability support. + *

+ * Don't set this field directly, but use the helper functions + * fuse_set_feature_flag() / fuse_unset_feature_flag() + *

+ * If the version of the loaded FUSE library is below 3.17, this method does nothing. + * + * @implSpec The default implementation always returns 0. + * @see #setFeatureFlag(long) + * @see #unsetFeatureFlag(long) + * @since libFUSE 3.17 + */ + default long wantExt() { + return 0; + } + + /** + * Sets the {@link #wantExt()} value. + *

+ * Deprecated way of setting the wantExt field. Use {@link #setFeatureFlag(long)} instead. + * If the version of the loaded FUSE library is below 3.17, this method does nothing. + * + * @param wantExt {@code want_ext} value + * @implSpec The default implementation is a no-op. + * @since libFUSE 3.17 + */ + default void setWantExt(long wantExt) { + //no-op + } + + /** + * Set a feature flag in the want_ext field of fuse_conn_info. + * + * @param flag feature flag to be set + * @return {@code true} if the flag was set, {@code false} if the flag is not supported + * @throws UnsupportedOperationException if the loaded fuse library does not implement the method + * @implSpec The default implementation throws {@link UnsupportedOperationException} + * @since libFUSE 3.17 + */ + default boolean setFeatureFlag(long flag) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Loaded library does not implement fuse_set_feature_flag"); + } + + /** + * Unset a feature flag in the want_ext field of fuse_conn_info. + * + * @param flag feature flag to be unset + * @throws UnsupportedOperationException if the loaded fuse library does not implement the method + * @implSpec The default implementation throws {@link UnsupportedOperationException} + * @since libFUSE 3.17 + */ + default void unsetFeatureFlag(long flag) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Loaded library does not implement fuse_unset_feature_flag"); + } + + /** + * Get the value of a feature flag in the want_ext field of fuse_conn_info. + * + * @param flag feature flag to be checked + * @return {@code true} if the flag is set, {@code false} otherwise + * @throws UnsupportedOperationException if the loaded fuse library does not implement the method + * @implSpec The default implementation throws {@link UnsupportedOperationException} + * @since libFUSE 3.17 + */ + default boolean getFeatureFlag(long flag) throws UnsupportedOperationException { + throw new UnsupportedOperationException("Loaded library does not implement fuse_get_feature_flag"); + } } diff --git a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl.java b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl.java index 8b20f86d..e0bff7c2 100644 --- a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl.java +++ b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl.java @@ -3,10 +3,20 @@ import org.cryptomator.jfuse.api.FuseConnInfo; import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_conn_info; -import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; -record FuseConnInfoImpl(MemorySegment segment) implements FuseConnInfo { +class FuseConnInfoImpl implements FuseConnInfo { + + protected final MemorySegment segment; + + FuseConnInfoImpl(MemorySegment segment) { + this.segment = segment; + } + + //mimic record behaviour + public MemorySegment segment() { + return segment; + } @Override public int protoMajor() { diff --git a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl317.java b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl317.java new file mode 100644 index 00000000..ce497b74 --- /dev/null +++ b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl317.java @@ -0,0 +1,43 @@ +package org.cryptomator.jfuse.linux.aarch64; + +import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_conn_info; + +import java.lang.foreign.MemorySegment; + +class FuseConnInfoImpl317 extends FuseConnInfoImpl { + + FuseConnInfoImpl317(MemorySegment segment) { + super(segment); + } + + @Override + public long capableExt() { + return fuse_conn_info.capable_ext(segment); + } + + @Override + public long wantExt() { + return fuse_conn_info.want_ext(segment); + } + + @Override + public void setWantExt(long wantExt) { + fuse_conn_info.want_ext(segment, wantExt); + } + + @Override + public boolean setFeatureFlag(long flag) { + return FuseFunctions.fuse_set_feature_flag(segment, flag); + } + + @Override + public void unsetFeatureFlag(long flag) { + FuseFunctions.fuse_unset_feature_flag(segment, flag); + } + + @Override + public boolean getFeatureFlag(long flag) { + return FuseFunctions.fuse_get_feature_flag(segment, flag); + } + +} diff --git a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseFunctions.java b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseFunctions.java index 52cada82..ce58a7a3 100644 --- a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseFunctions.java +++ b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseFunctions.java @@ -5,21 +5,30 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.SymbolLookup; import java.lang.invoke.MethodHandle; +import java.util.Optional; import static java.lang.foreign.ValueLayout.ADDRESS; import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.JAVA_LONG; /** * These method references can not be jextract'ed, partly due to jextract not being able to understand {@code #define}, * partly due to slight differences in the FUSE API, which applies a versioning scheme via dlvsym, that Panama's default - * {@link java.lang.foreign.SymbolLookup} doesn't support. + * {@link SymbolLookup} doesn't support. */ class FuseFunctions { // see https://github.com/libfuse/libfuse/blob/fuse-3.12.0/include/fuse_lowlevel.h#L1892-L1923 private static final FunctionDescriptor FUSE_PARSE_CMDLINE = FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS); + //https://github.com/libfuse/libfuse/blob/fuse-3.17.4/lib/fuse_lowlevel.c#L2035 + private static final FunctionDescriptor FUSE_SET_FEATURE_FLAG = FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_LONG); + private static final FunctionDescriptor FUSE_UNSET_FEATURE_FLAG = FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG); + private static final FunctionDescriptor FUSE_GET_FEATURE_FLAG = FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_LONG); private final MethodHandle fuse_parse_cmdline; + private final Optional fuse_set_feature_flag; + private final Optional fuse_unset_feature_flag; + private final Optional fuse_get_feature_flag; private FuseFunctions() { var lookup = SymbolLookup.loaderLookup(); @@ -27,6 +36,12 @@ private FuseFunctions() { this.fuse_parse_cmdline = lookup.find("fuse_parse_cmdline") .map(symbol -> linker.downcallHandle(symbol, FUSE_PARSE_CMDLINE)) .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol fuse_parse_cmdline")); + this.fuse_set_feature_flag = lookup.find("fuse_set_feature_flag") + .map(symbol -> linker.downcallHandle(symbol, FUSE_SET_FEATURE_FLAG)); + this.fuse_unset_feature_flag = lookup.find("fuse_unset_feature_flag") + .map(symbol -> linker.downcallHandle(symbol, FUSE_UNSET_FEATURE_FLAG)); + this.fuse_get_feature_flag = lookup.find("fuse_get_feature_flag") + .map(symbol -> linker.downcallHandle(symbol, FUSE_GET_FEATURE_FLAG)); } private static class Holder { @@ -41,4 +56,31 @@ public static int fuse_parse_cmdline(MemorySegment args, MemorySegment opts) { } } + public static boolean fuse_set_feature_flag(MemorySegment fuse_conn_info, long flag) throws UnsupportedOperationException { + var method = Holder.INSTANCE.fuse_set_feature_flag.orElseThrow(() -> new UnsupportedOperationException("The loaded fuse library does not implement fuse_set_feature_flag")); + try { + return ((int) method.invokeExact(fuse_conn_info, flag)) != 0; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + + public static void fuse_unset_feature_flag(MemorySegment fuse_conn_info, long flag) throws UnsupportedOperationException { + var method = Holder.INSTANCE.fuse_unset_feature_flag.orElseThrow(() -> new UnsupportedOperationException("The loaded fuse library does not implement fuse_unset_feature_flag")); + try { + method.invokeExact(fuse_conn_info, flag); + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + + public static boolean fuse_get_feature_flag(MemorySegment fuse_conn_info, long flag) throws UnsupportedOperationException { + var method = Holder.INSTANCE.fuse_get_feature_flag.orElseThrow(() -> new UnsupportedOperationException("The loaded fuse library does not implement fuse_get_feature_flag")); + try { + return ((int) method.invokeExact(fuse_conn_info, flag)) != 0; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + } diff --git a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseImpl.java b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseImpl.java index e529f481..f48757e8 100644 --- a/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseImpl.java +++ b/jfuse-linux-aarch64/src/main/java/org/cryptomator/jfuse/linux/aarch64/FuseImpl.java @@ -109,7 +109,12 @@ protected void bind(FuseOperations.Operation operation) { @VisibleForTesting MemorySegment init(MemorySegment conn, MemorySegment cfg) { var connInfo = new FuseConnInfoImpl(conn); - connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS); + if (fuse_h.fuse_version() >= 317) { + connInfo = new FuseConnInfoImpl317(conn); + connInfo.setFeatureFlag(FuseConnInfo.FUSE_CAP_READDIRPLUS); + } else { + connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS); + } var config = new FuseConfigImpl(cfg); fuseOperations.init(connInfo, config); return MemorySegment.NULL; diff --git a/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl317Test.java b/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl317Test.java new file mode 100644 index 00000000..fff8cc55 --- /dev/null +++ b/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseConnInfoImpl317Test.java @@ -0,0 +1,72 @@ +package org.cryptomator.jfuse.linux.aarch64; + +import org.cryptomator.jfuse.api.FuseConnInfo; +import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_conn_info; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Stream; + +public class FuseConnInfoImpl317Test { + + @DisplayName("test long getters") + @ParameterizedTest(name = "{1}") + @MethodSource + void testGetters(SetInMemorySegment setter, GetInConnInfo getter) { + try (var arena = Arena.ofConfined()) { + var segment = fuse_conn_info.allocate(arena); + var connInfo = new FuseConnInfoImpl317(segment); + + setter.accept(segment, 42L); + + Assertions.assertEquals(42L, getter.apply(connInfo)); + } + } + + public static Stream testGetters() { + return Stream.of( + Arguments.arguments((SetInMemorySegment) fuse_conn_info::want_ext, Named.of("wantExt()", (GetInConnInfo) FuseConnInfo::wantExt)), + Arguments.arguments((SetInMemorySegment) fuse_conn_info::capable_ext, Named.of("capableExt()", (GetInConnInfo) FuseConnInfo::capableExt)) + ); + } + + interface SetInMemorySegment extends BiConsumer { + } + + interface GetInConnInfo extends Function { + } + + @DisplayName("test setters") + @ParameterizedTest(name = "{0}") + @MethodSource + void testSetters(SetInConnInfo setter, GetInMemorySegment getter) { + try (var arena = Arena.ofConfined()) { + var segment = fuse_conn_info.allocate(arena); + var connInfo = new FuseConnInfoImpl317(segment); + + setter.accept(connInfo, 42L); + + Assertions.assertEquals(42L, getter.apply(segment)); + } + } + + public static Stream testSetters() { + return Stream.of( + Arguments.arguments(Named.of("setWantExt()", (SetInConnInfo) FuseConnInfo::setWantExt), (GetInMemorySegment) fuse_conn_info::want_ext) + ); + } + + interface SetInConnInfo extends BiConsumer { + } + + interface GetInMemorySegment extends Function { + } +} diff --git a/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseImplTest.java b/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseImplTest.java index 626d26a8..2235b1a4 100644 --- a/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseImplTest.java +++ b/jfuse-linux-aarch64/src/test/java/org/cryptomator/jfuse/linux/aarch64/FuseImplTest.java @@ -4,15 +4,13 @@ import org.cryptomator.jfuse.api.FuseMountFailedException; import org.cryptomator.jfuse.api.FuseOperations; import org.cryptomator.jfuse.api.TimeSpec; -import org.cryptomator.jfuse.linux.aarch64.extr.fuse3_lowlevel.fuse_cmdline_opts; import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_config; import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_conn_info; import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_file_info; import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.fuse_h; import org.cryptomator.jfuse.linux.aarch64.extr.fuse3.timespec; -import org.junit.jupiter.api.AfterEach; +import org.cryptomator.jfuse.linux.aarch64.extr.fuse3_lowlevel.fuse_cmdline_opts; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -20,14 +18,13 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Answers; -import org.mockito.MockedStatic; import org.mockito.Mockito; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.time.Instant; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; public class FuseImplTest { @@ -162,22 +159,59 @@ public void testFsyncdir() { } - @DisplayName("init() sets fuse_conn_info.wants |= FUSE_CAP_READDIRPLUS") - @Test - public void testInit() { - try (var arena = Arena.ofConfined()) { - var result = new AtomicInteger(); - Mockito.doAnswer(invocation -> { - FuseConnInfo connInfo = invocation.getArgument(0); - result.set(connInfo.want()); - return null; - }).when(fuseOps).init(Mockito.any(), Mockito.any()); - var connInfo = fuse_conn_info.allocate(arena); - var fuseConfig = fuse_config.allocate(arena); - - fuseImpl.init(connInfo, fuseConfig); - - Assertions.assertEquals(FuseConnInfo.FUSE_CAP_READDIRPLUS, result.get() & FuseConnInfo.FUSE_CAP_READDIRPLUS); + @Nested + @DisplayName("init") + public class Init { + + @DisplayName("init() sets fuse_conn_info.wants |= FUSE_CAP_READDIRPLUS for libfuse < 3.17") + @Test + public void testInit316() { + try (var fuseFunctionsClass = Mockito.mockStatic(FuseFunctions.class); + var fuseH = Mockito.mockStatic(fuse_h.class); + var arena = Arena.ofConfined()) { + + var consumerReceivedConnInfo = new AtomicReference(); + Mockito.doAnswer(invocation -> { + consumerReceivedConnInfo.set(invocation.getArgument(0)); + return null; + }).when(fuseOps).init(Mockito.any(), Mockito.any()); + var connInfo = fuse_conn_info.allocate(arena); + var fuseConfig = fuse_config.allocate(arena); + + fuseH.when(fuse_h::fuse_version).thenReturn(316); + fuseFunctionsClass.when(() -> FuseFunctions.fuse_set_feature_flag(Mockito.any(), Mockito.anyLong())).thenThrow(UnsupportedOperationException.class); + + fuseImpl.init(connInfo, fuseConfig); + + Assertions.assertInstanceOf(FuseConnInfoImpl.class, consumerReceivedConnInfo.get()); + Assertions.assertEquals(FuseConnInfo.FUSE_CAP_READDIRPLUS, consumerReceivedConnInfo.get().want() & FuseConnInfo.FUSE_CAP_READDIRPLUS); + } + } + + @DisplayName("init() calls set_feature_flag(FUSE_CAP_READDIRPLUS) for libfuse >= 3.17") + @Test + public void testInit317() { + try (var fuseFunctionsClass = Mockito.mockStatic(FuseFunctions.class); + var fuseH = Mockito.mockStatic(fuse_h.class); + var arena = Arena.ofConfined()) { + + var consumerReceivedConnInfo = new AtomicReference(); + Mockito.doAnswer(invocation -> { + consumerReceivedConnInfo.set(invocation.getArgument(0)); + return null; + }).when(fuseOps).init(Mockito.any(), Mockito.any()); + var connInfo = fuse_conn_info.allocate(arena); + var fuseConfig = fuse_config.allocate(arena); + + fuseH.when(fuse_h::fuse_version).thenReturn(317); + fuseFunctionsClass.when(() -> FuseFunctions.fuse_set_feature_flag(connInfo, FuseConnInfo.FUSE_CAP_READDIRPLUS)).thenReturn(true); + + + fuseImpl.init(connInfo, fuseConfig); + + fuseFunctionsClass.verify(() -> FuseFunctions.fuse_set_feature_flag(connInfo, FuseConnInfo.FUSE_CAP_READDIRPLUS)); + Assertions.assertInstanceOf(FuseConnInfoImpl317.class, consumerReceivedConnInfo.get()); + } } } diff --git a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl.java b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl.java index 487e0c44..071e6761 100644 --- a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl.java +++ b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl.java @@ -3,10 +3,20 @@ import org.cryptomator.jfuse.api.FuseConnInfo; import org.cryptomator.jfuse.linux.amd64.extr.fuse3.fuse_conn_info; -import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; -record FuseConnInfoImpl(MemorySegment segment) implements FuseConnInfo { +class FuseConnInfoImpl implements FuseConnInfo { + + protected final MemorySegment segment; + + FuseConnInfoImpl(MemorySegment segment) { + this.segment = segment; + } + + //mimic record behaviour + public MemorySegment segment() { + return segment; + } @Override public int protoMajor() { diff --git a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl317.java b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl317.java new file mode 100644 index 00000000..44a23568 --- /dev/null +++ b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl317.java @@ -0,0 +1,45 @@ +package org.cryptomator.jfuse.linux.amd64; + +import org.cryptomator.jfuse.linux.amd64.extr.fuse3.fuse_conn_info; + +import java.lang.foreign.MemorySegment; + +class FuseConnInfoImpl317 extends FuseConnInfoImpl { + + FuseConnInfoImpl317(MemorySegment segment) { + super(segment); + } + + @Override + public long capableExt() { + //old school style + //return segment.get(fuse_conn_info.capable_ext$layout().withByteAlignment(1), fuse_conn_info.capable_ext$offset()); + return fuse_conn_info.capable_ext(segment); + } + + @Override + public long wantExt() { + return fuse_conn_info.want_ext(segment); + } + + @Override + public void setWantExt(long wantExt) { + fuse_conn_info.want_ext(segment, wantExt); + } + + @Override + public boolean setFeatureFlag(long flag) { + return FuseFunctions.fuse_set_feature_flag(segment, flag); + } + + @Override + public void unsetFeatureFlag(long flag) { + FuseFunctions.fuse_unset_feature_flag(segment, flag); + } + + @Override + public boolean getFeatureFlag(long flag) { + return FuseFunctions.fuse_get_feature_flag(segment, flag); + } + +} diff --git a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseFunctions.java b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseFunctions.java index 4a182345..490a131b 100644 --- a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseFunctions.java +++ b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseFunctions.java @@ -5,9 +5,11 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.SymbolLookup; import java.lang.invoke.MethodHandle; +import java.util.Optional; import static java.lang.foreign.ValueLayout.ADDRESS; import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.JAVA_LONG; /** * These method references can not be jextract'ed, partly due to jextract not being able to understand {@code #define}, @@ -18,8 +20,15 @@ class FuseFunctions { // see https://github.com/libfuse/libfuse/blob/fuse-3.12.0/include/fuse_lowlevel.h#L1892-L1923 private static final FunctionDescriptor FUSE_PARSE_CMDLINE = FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS); + //https://github.com/libfuse/libfuse/blob/fuse-3.17.4/lib/fuse_lowlevel.c#L2035 + private static final FunctionDescriptor FUSE_SET_FEATURE_FLAG = FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_LONG); + private static final FunctionDescriptor FUSE_UNSET_FEATURE_FLAG = FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG); + private static final FunctionDescriptor FUSE_GET_FEATURE_FLAG = FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_LONG); private final MethodHandle fuse_parse_cmdline; + private final Optional fuse_set_feature_flag; + private final Optional fuse_unset_feature_flag; + private final Optional fuse_get_feature_flag; private FuseFunctions() { var lookup = SymbolLookup.loaderLookup(); @@ -27,6 +36,12 @@ private FuseFunctions() { this.fuse_parse_cmdline = lookup.find("fuse_parse_cmdline") .map(symbol -> linker.downcallHandle(symbol, FUSE_PARSE_CMDLINE)) .orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol fuse_parse_cmdline")); + this.fuse_set_feature_flag = lookup.find("fuse_set_feature_flag") + .map(symbol -> linker.downcallHandle(symbol, FUSE_SET_FEATURE_FLAG)); + this.fuse_unset_feature_flag = lookup.find("fuse_unset_feature_flag") + .map(symbol -> linker.downcallHandle(symbol, FUSE_UNSET_FEATURE_FLAG)); + this.fuse_get_feature_flag = lookup.find("fuse_get_feature_flag") + .map(symbol -> linker.downcallHandle(symbol, FUSE_GET_FEATURE_FLAG)); } private static class Holder { @@ -41,4 +56,31 @@ public static int fuse_parse_cmdline(MemorySegment args, MemorySegment opts) { } } + public static boolean fuse_set_feature_flag(MemorySegment fuse_conn_info, long flag) throws UnsupportedOperationException { + var method = Holder.INSTANCE.fuse_set_feature_flag.orElseThrow(() -> new UnsupportedOperationException("The loaded fuse library does not implement fuse_set_feature_flag")); + try { + return ((int) method.invokeExact(fuse_conn_info, flag)) != 0; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + + public static void fuse_unset_feature_flag(MemorySegment fuse_conn_info, long flag) throws UnsupportedOperationException { + var method = Holder.INSTANCE.fuse_unset_feature_flag.orElseThrow(() -> new UnsupportedOperationException("The loaded fuse library does not implement fuse_unset_feature_flag")); + try { + method.invokeExact(fuse_conn_info, flag); + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + + public static boolean fuse_get_feature_flag(MemorySegment fuse_conn_info, long flag) throws UnsupportedOperationException { + var method = Holder.INSTANCE.fuse_get_feature_flag.orElseThrow(() -> new UnsupportedOperationException("The loaded fuse library does not implement fuse_get_feature_flag")); + try { + return ((int) method.invokeExact(fuse_conn_info, flag)) != 0; + } catch (Throwable e) { + throw new AssertionError("should not reach here", e); + } + } + } diff --git a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseImpl.java b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseImpl.java index 43ea654e..f62038b0 100644 --- a/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseImpl.java +++ b/jfuse-linux-amd64/src/main/java/org/cryptomator/jfuse/linux/amd64/FuseImpl.java @@ -109,7 +109,12 @@ protected void bind(FuseOperations.Operation operation) { @VisibleForTesting MemorySegment init(MemorySegment conn, MemorySegment cfg) { var connInfo = new FuseConnInfoImpl(conn); - connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS); + if (fuse_h.fuse_version() >= 317) { + connInfo = new FuseConnInfoImpl317(conn); + connInfo.setFeatureFlag(FuseConnInfo.FUSE_CAP_READDIRPLUS); + } else { + connInfo.setWant(connInfo.want() | FuseConnInfo.FUSE_CAP_READDIRPLUS); + } var config = new FuseConfigImpl(cfg); fuseOperations.init(connInfo, config); return MemorySegment.NULL; diff --git a/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl317Test.java b/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl317Test.java new file mode 100644 index 00000000..134b73f9 --- /dev/null +++ b/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseConnInfoImpl317Test.java @@ -0,0 +1,72 @@ +package org.cryptomator.jfuse.linux.amd64; + +import org.cryptomator.jfuse.api.FuseConnInfo; +import org.cryptomator.jfuse.linux.amd64.extr.fuse3.fuse_conn_info; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.stream.Stream; + +public class FuseConnInfoImpl317Test { + + @DisplayName("test long getters") + @ParameterizedTest(name = "{1}") + @MethodSource + void testGetters(SetInMemorySegment setter, GetInConnInfo getter) { + try (var arena = Arena.ofConfined()) { + var segment = fuse_conn_info.allocate(arena); + var connInfo = new FuseConnInfoImpl317(segment); + + setter.accept(segment, 42L); + + Assertions.assertEquals(42L, getter.apply(connInfo)); + } + } + + public static Stream testGetters() { + return Stream.of( + Arguments.arguments((SetInMemorySegment) fuse_conn_info::want_ext, Named.of("wantExt()", (GetInConnInfo) FuseConnInfo::wantExt)), + Arguments.arguments((SetInMemorySegment) fuse_conn_info::capable_ext, Named.of("capableExt()", (GetInConnInfo) FuseConnInfo::capableExt)) + ); + } + + interface SetInMemorySegment extends BiConsumer { + } + + interface GetInConnInfo extends Function { + } + + @DisplayName("test setters") + @ParameterizedTest(name = "{0}") + @MethodSource + void testSetters(SetInConnInfo setter, GetInMemorySegment getter) { + try (var arena = Arena.ofConfined()) { + var segment = fuse_conn_info.allocate(arena); + var connInfo = new FuseConnInfoImpl317(segment); + + setter.accept(connInfo, 42L); + + Assertions.assertEquals(42L, getter.apply(segment)); + } + } + + public static Stream testSetters() { + return Stream.of( + Arguments.arguments(Named.of("setWantExt()", (SetInConnInfo) FuseConnInfo::setWantExt), (GetInMemorySegment) fuse_conn_info::want_ext) + ); + } + + interface SetInConnInfo extends BiConsumer { + } + + interface GetInMemorySegment extends Function { + } +} diff --git a/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseImplTest.java b/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseImplTest.java index d93b71b3..32afa877 100644 --- a/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseImplTest.java +++ b/jfuse-linux-amd64/src/test/java/org/cryptomator/jfuse/linux/amd64/FuseImplTest.java @@ -24,7 +24,7 @@ import java.lang.foreign.MemorySegment; import java.time.Instant; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; public class FuseImplTest { @@ -159,22 +159,59 @@ public void testFsyncdir() { } - @DisplayName("init() sets fuse_conn_info.wants |= FUSE_CAP_READDIRPLUS") - @Test - public void testInit() { - try (var arena = Arena.ofConfined()) { - var result = new AtomicInteger(); - Mockito.doAnswer(invocation -> { - FuseConnInfo connInfo = invocation.getArgument(0); - result.set(connInfo.want()); - return null; - }).when(fuseOps).init(Mockito.any(), Mockito.any()); - var connInfo = fuse_conn_info.allocate(arena); - var fuseConfig = fuse_config.allocate(arena); - - fuseImpl.init(connInfo, fuseConfig); - - Assertions.assertEquals(FuseConnInfo.FUSE_CAP_READDIRPLUS, result.get() & FuseConnInfo.FUSE_CAP_READDIRPLUS); + @Nested + @DisplayName("init") + public class Init { + + @DisplayName("init() sets fuse_conn_info.wants |= FUSE_CAP_READDIRPLUS for libfuse < 3.17") + @Test + public void testInit316() { + try (var fuseFunctionsClass = Mockito.mockStatic(FuseFunctions.class); + var fuseH = Mockito.mockStatic(fuse_h.class); + var arena = Arena.ofConfined()) { + + var consumerReceivedConnInfo = new AtomicReference(); + Mockito.doAnswer(invocation -> { + consumerReceivedConnInfo.set(invocation.getArgument(0)); + return null; + }).when(fuseOps).init(Mockito.any(), Mockito.any()); + var connInfo = fuse_conn_info.allocate(arena); + var fuseConfig = fuse_config.allocate(arena); + + fuseH.when(fuse_h::fuse_version).thenReturn(316); + fuseFunctionsClass.when(() -> FuseFunctions.fuse_set_feature_flag(Mockito.any(), Mockito.anyLong())).thenThrow(UnsupportedOperationException.class); + + fuseImpl.init(connInfo, fuseConfig); + + Assertions.assertInstanceOf(FuseConnInfoImpl.class, consumerReceivedConnInfo.get()); + Assertions.assertEquals(FuseConnInfo.FUSE_CAP_READDIRPLUS, consumerReceivedConnInfo.get().want() & FuseConnInfo.FUSE_CAP_READDIRPLUS); + } + } + + @DisplayName("init() calls set_feature_flag(FUSE_CAP_READDIRPLUS) for libfuse >= 3.17") + @Test + public void testInit317() { + try (var fuseFunctionsClass = Mockito.mockStatic(FuseFunctions.class); + var fuseH = Mockito.mockStatic(fuse_h.class); + var arena = Arena.ofConfined()) { + + var consumerReceivedConnInfo = new AtomicReference(); + Mockito.doAnswer(invocation -> { + consumerReceivedConnInfo.set(invocation.getArgument(0)); + return null; + }).when(fuseOps).init(Mockito.any(), Mockito.any()); + var connInfo = fuse_conn_info.allocate(arena); + var fuseConfig = fuse_config.allocate(arena); + + fuseH.when(fuse_h::fuse_version).thenReturn(317); + fuseFunctionsClass.when(() -> FuseFunctions.fuse_set_feature_flag(connInfo, FuseConnInfo.FUSE_CAP_READDIRPLUS)).thenReturn(true); + + + fuseImpl.init(connInfo, fuseConfig); + + fuseFunctionsClass.verify(() -> FuseFunctions.fuse_set_feature_flag(connInfo, FuseConnInfo.FUSE_CAP_READDIRPLUS)); + Assertions.assertInstanceOf(FuseConnInfoImpl317.class, consumerReceivedConnInfo.get()); + } } } diff --git a/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java b/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java index e85e93ad..03bc4205 100644 --- a/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java +++ b/jfuse-win/src/main/java/org/cryptomator/jfuse/win/FuseImpl.java @@ -14,7 +14,6 @@ import org.jetbrains.annotations.VisibleForTesting; import java.lang.foreign.Arena; -import java.lang.foreign.MemoryLayout; import java.lang.foreign.MemorySegment; import java.lang.foreign.ValueLayout; import java.nio.file.Path; diff --git a/pom.xml b/pom.xml index 681d508d..a73653b2 100644 --- a/pom.xml +++ b/pom.xml @@ -119,6 +119,25 @@ org.apache.maven.plugins maven-javadoc-plugin 3.11.2 + + + + apiNote + a + API Note: + + + implSpec + a + Implementation Requirements: + + + implNote + a + Implementation Note: + + + org.apache.maven.plugins