From 54fa9cb8ace5ab3a2fbb3a47a68c9f969eb22b52 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 6 Oct 2026 15:44:37 +0200 Subject: [PATCH] Use native-bootstrap to find folder for safer natives extraction --- gradle/libs.versions.toml | 4 +- jme3-desktop/build.gradle | 1 + .../jme3/system/NativeLibraryExtraction.java | 167 ------------------ .../com/jme3/system/NativeLibraryLoader.java | 163 +++++++---------- .../system/NativeLibraryExtractionTest.java | 23 ++- .../NativeLibraryLoaderExtractionTest.java | 140 +++++++++++++-- .../jme3/system/NativeLibraryLoaderTest.java | 10 ++ 7 files changed, 222 insertions(+), 286 deletions(-) delete mode 100644 jme3-desktop/src/main/java/com/jme3/system/NativeLibraryExtraction.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 963275a52e..d58d1591ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,8 @@ jacoco = "0.8.12" lwjgl3 = "3.4.3" angle = "2026-05-09" libjglios = "0.10" -saferalloc = "0.0.10" +saferalloc = "0.0.11" +native-bootstrap = "0.1.1" nifty = "1.4.3" spotbugs = "4.9.8" jmeAndroidNatives = "3.10.0-xt16kb-alloc" @@ -79,6 +80,7 @@ libjglios-gles-ios = { module = "org.ngengine:libjglios-gles-ios", version.ref = libjglios-gradle-plugin = { module = "org.ngengine:libjglios-gradle-plugin", version.ref = "libjglios" } libjglios-openal-ios = { module = "org.ngengine:libjglios-openal-ios", version.ref = "libjglios" } libjglios-sdl3-ios = { module = "org.ngengine:libjglios-sdl3-ios", version.ref = "libjglios" } +native-directories = { module = "org.jmonkeyengine:native-directories", version.ref = "native-bootstrap" } saferalloc = { module = "org.ngengine:saferalloc", version.ref = "saferalloc" } saferalloc-natives-linux-x8664 = { module = "org.ngengine:saferalloc-natives-linux-x86_64", version.ref = "saferalloc" } saferalloc-natives-linux-aarch64 = { module = "org.ngengine:saferalloc-natives-linux-aarch64", version.ref = "saferalloc" } diff --git a/jme3-desktop/build.gradle b/jme3-desktop/build.gradle index 4c15d96d77..650436b8eb 100644 --- a/jme3-desktop/build.gradle +++ b/jme3-desktop/build.gradle @@ -1,6 +1,7 @@ dependencies { api project(':jme3-core') api project(':jme3-plugins') + implementation libs.native.directories } tasks.register('scanJ3O', JavaExec) { diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryExtraction.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryExtraction.java deleted file mode 100644 index 6e48a8eace..0000000000 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryExtraction.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (c) 2009-2023 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.jme3.system; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.AclEntry; -import java.nio.file.attribute.AclEntryFlag; -import java.nio.file.attribute.AclEntryPermission; -import java.nio.file.attribute.AclEntryType; -import java.nio.file.attribute.AclFileAttributeView; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.PosixFileAttributes; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.nio.file.attribute.UserPrincipal; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; - -/** Creates isolated extraction directories without trusting predictable cache files. */ -final class NativeLibraryExtraction { - private NativeLibraryExtraction() {} - - static Path createDirectory(Path root, String prefix) throws IOException { - Path existing = root.toAbsolutePath(); - while (!Files.exists(existing)) { - existing = existing.getParent(); - if (existing == null) throw new IOException("No existing ancestor for " + root); - } - FileAttribute permissions = privatePermissions(existing); - Files.createDirectories(root, permissions); - // Resolve symlinks such as macOS /var -> /private/var before using the path. - root = root.toRealPath(); - if (Files.getFileStore(root).supportsFileAttributeView("posix")) { - checkPosixParents(root); - } - Path directory = Files.createTempDirectory(root, prefix, permissions); - boolean usable = false; - try { - directory.toFile().deleteOnExit(); - if (permissions.name().equals("acl:acl")) { - // Remove any inherited grants before extracting native code. The initial - // ACL also requests owner-only access at creation time. - AclFileAttributeView view = Files.getFileAttributeView(directory, AclFileAttributeView.class); - @SuppressWarnings("unchecked") - List acl = (List) permissions.value(); - view.setAcl(acl); - for (AclEntry entry : view.getAcl()) { - if (entry.type() == AclEntryType.ALLOW && !entry.principal().equals(acl.get(0).principal())) { - throw new IOException("Cannot restrict native directory ACL: " + directory); - } - } - } - checkExecutable(directory); - usable = true; - return directory; - } finally { - if (!usable) { - Files.deleteIfExists(directory); - } - } - } - - private static void checkPosixParents(Path root) throws IOException { - UserPrincipal user = root.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - List parents = new ArrayList<>(); - for (Path parent = root; parent != null; parent = parent.getParent()) parents.add(parent); - Collections.reverse(parents); - boolean privateAncestor = false; - for (Path parent : parents) { - PosixFileAttributes attributes = Files.readAttributes(parent, PosixFileAttributes.class); - // A private child is not safe if another user can rename/replace its parent. - boolean systemOwned = attributes.owner().getName().equals("root"); - if (!attributes.owner().equals(user) && !systemOwned) { - throw new IOException("Native extraction ancestor belongs to another user: " + parent); - } - if (!privateAncestor && (attributes.permissions().contains(PosixFilePermission.GROUP_WRITE) - || attributes.permissions().contains(PosixFilePermission.OTHERS_WRITE))) { - // Shared system temp directories are safe only with the sticky bit. - int mode = ((Number) Files.getAttribute(parent, "unix:mode")).intValue(); - if ((mode & 01000) == 0) { - throw new IOException("Native extraction ancestor is writable by other users: " + parent); - } - } - // Permissions below an owner-only directory cannot grant outsiders access. - if (attributes.owner().equals(user) - && !attributes.permissions().contains(PosixFilePermission.GROUP_EXECUTE) - && !attributes.permissions().contains(PosixFilePermission.OTHERS_EXECUTE)) { - privateAncestor = true; - } - } - } - - private static FileAttribute privatePermissions(Path root) throws IOException { - if (Files.getFileStore(root).supportsFileAttributeView("posix")) { - return PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); - } - if (Files.getFileStore(root).supportsFileAttributeView("acl")) { - UserPrincipal owner = root.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - final List acl = Collections.singletonList(AclEntry.newBuilder() - .setType(AclEntryType.ALLOW) - .setPrincipal(owner) - .setPermissions(EnumSet.allOf(AclEntryPermission.class)) - .setFlags(AclEntryFlag.FILE_INHERIT, AclEntryFlag.DIRECTORY_INHERIT) - .build()); - return new FileAttribute>() { - @Override - public String name() { return "acl:acl"; } - @Override - public List value() { return acl; } - }; - } - throw new IOException("Filesystem cannot create private native directories: " + root); - } - - private static void checkExecutable(Path directory) throws IOException { - if (!Files.getFileStore(directory).supportsFileAttributeView("posix")) { - // Windows DLL loading is checked by the native loader, not Unix execute bits. - return; - } - Path probe = Files.createTempFile(directory, "exec-probe-", null); - try { - Files.setPosixFilePermissions(probe, PosixFilePermissions.fromString("rwx------")); - // On Linux, access(X_OK) on a regular file detects noexec. Checking the - // directory itself only checks traversal. No subprocess is executed. - if (!Files.isExecutable(probe)) { - throw new IOException("Native execution is not permitted (possibly noexec): " + directory); - } - } finally { - Files.deleteIfExists(probe); - } - } -} diff --git a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java index a3999ee34d..0733e30646 100644 --- a/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java +++ b/jme3-desktop/src/main/java/com/jme3/system/NativeLibraryLoader.java @@ -39,6 +39,8 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -47,6 +49,9 @@ import com.jme3.system.NativeLibraries.LibraryInfo; import com.jme3.util.res.Resources; +import com.jme3.nativebootstrap.common.OperatingSystem; +import com.jme3.nativebootstrap.directories.NativeDirectories; +import com.jme3.nativebootstrap.directories.DirectoryCandidate; /** * Utility class to register, extract, and load native libraries. @@ -91,15 +96,16 @@ public final class NativeLibraryLoader { private static final Logger logger = Logger.getLogger(NativeLibraryLoader.class.getName()); private static File extractionFolderOverride = null; private static File extractionFolder = null; - private static final int EXTRACTION_ROOT_COUNT = 4; // configured, temp, cache, home + private static List extractionCandidates = null; private static int extractionRootIndex = 0; // extraction root to try next private static Boolean extractNativeLibrariesOverride = null; private static final Map loadedLibraries = new HashMap<>(); private static final HashMap nativeLibraryMap = new HashMap<>(); - static { - NativeLibraries.registerDefaultLibraries(); + static { + configureNativeProperties(); + NativeLibraries.registerDefaultLibraries(); } /** @@ -196,6 +202,8 @@ public static synchronized void setCustomExtractionFolder(String path) { extractionFolderOverride = path == null ? null : new File(path).getAbsoluteFile(); extractionFolder = null; extractionRootIndex = 0; + extractionCandidates = null; + configureNativeProperties(); } /** @@ -274,116 +282,68 @@ public static synchronized File getExtractionFolder() { UnsatisfiedLinkError error = new UnsatisfiedLinkError( "Cannot find a suitable extraction folder for native libraries."); - exit: - while (true) { - Path root = null; + if (extractionCandidates == null) { try { - switch (extractionRootIndex) { - case 0: { // configured directory - File custom = getCustomExtractionFolder(); - if (custom == null) { - extractionRootIndex++; - continue; - } - root = custom.toPath(); - break; - } - case 1: { // temp directory - String tmp = System.getProperty("java.io.tmpdir", "").trim(); - if (tmp.isEmpty()) throw new IllegalArgumentException("java.io.tmpdir is not set"); - root = Paths.get(tmp); - if (!root.isAbsolute() || !Files.isDirectory(root)) throw new IllegalArgumentException("java.io.tmpdir is not a valid extraction root"); - break; - } - case 2: { // platform cache - root = getJmeUserCacheFolder(); - if (root == null) throw new IOException("No usable cache directory"); - root = root.resolve(".jme3"); - break; - } - case 3: { // user home directory - String home = System.getProperty("user.home", "").trim(); - if (home.isEmpty()) throw new IllegalArgumentException("user.home is not set"); - root = Paths.get(home); - if (!root.isAbsolute() || (root.getParent() != null - && !Files.isDirectory(root.getParent()))) throw new IllegalArgumentException("user.home is not a valid extraction root"); - root = root.resolve(".jme3"); - break; - } - default: { - break exit; - } - } - - extractionFolder = NativeLibraryExtraction.createDirectory(root, "jme3-natives-").toFile(); - return extractionFolder; - } catch (IOException | SecurityException | IllegalArgumentException + extractionCandidates = getExtractionCandidates(); + } catch (SecurityException | IllegalArgumentException | UnsupportedOperationException failure) { + error.addSuppressed(failure); + throw error; + } + } + while (extractionRootIndex < extractionCandidates.size()) { + DirectoryCandidate candidate = extractionCandidates.get(extractionRootIndex); + try { + File directory = candidate.get().toFile(); + extractionFolder = directory; + return directory; + } catch (UncheckedIOException | SecurityException | IllegalArgumentException | UnsupportedOperationException failure) { - error.addSuppressed(new IOException("Cannot use native extraction root: " + root, failure)); + error.addSuppressed(new IOException("Cannot use native extraction root: " + candidate.root(), failure)); } - extractionRootIndex++; // next call tries the next root + extractionRootIndex++; } - extractionRootIndex = 0; // let a later call try every root again + extractionRootIndex = 0; + extractionCandidates = null; throw error; } - /** - * Returns the platform cache folder - */ - private static Path getJmeUserCacheFolder() { - Path base = null; - String cacheFolder = System.getProperty(CACHE_FOLDER_PROPERTY); - if (cacheFolder != null && !cacheFolder.trim().isEmpty()) { - base = Paths.get(cacheFolder); - if (!base.isAbsolute() || !Files.isDirectory(base)) { - base = null; + private static List getExtractionCandidates() { + configureNativeProperties(); + List candidates = new ArrayList<>( + NativeDirectories.candidates("jme3", NativeLibraryLoader::getOperatingSystem)); + File custom = getCustomExtractionFolder(); + if (custom != null) { + Path customPath = custom.toPath(); + candidates.removeIf(candidate -> candidate.root().equals(customPath.toAbsolutePath().normalize())); + candidates.add(0, new DirectoryCandidate(customPath, "jme3", getOperatingSystem())); + } + return candidates; + } + + private static void configureNativeProperties() { + String cache = System.getProperty(CACHE_FOLDER_PROPERTY); + if (cache != null && !cache.trim().isEmpty()) { + File directory = new File(cache); + if (!directory.isAbsolute() || !directory.isDirectory()) { logger.warning(CACHE_FOLDER_PROPERTY + " must be an absolute path and must exist. Falling back to default cache location."); + return; } + System.setProperty("natives.cacheDir", cache); } + } - if (base == null) { - String loc = null; - Platform.Os os = JmeSystem.getPlatform().getOs(); - if (os == Platform.Os.Windows) { - loc = System.getenv("LOCALAPPDATA"); - } else if (os == Platform.Os.Linux) { - loc = System.getenv("XDG_CACHE_HOME"); - } - - if (loc != null && !loc.trim().isEmpty()) { - base = Paths.get(loc); - if (!base.isAbsolute() || !Files.isDirectory(base)) base = null; - } - } - - if (base == null) { - Platform.Os os = JmeSystem.getPlatform().getOs(); - Path home = Paths.get(System.getProperty("user.home")); - try{ - switch (os) { - case Windows: - base = home.resolve("AppData").resolve("Local"); - break; - case MacOS: - base = home.resolve("Library").resolve("Caches"); - break; - default: - base = home.resolve(".cache"); - break; - } - } catch (Exception e) { - logger.warning("Failed to determine default cache location: " + e.getMessage()); - } - - if (base != null && (!base.isAbsolute() || !Files.isDirectory(base))) { - base = null; - } - } + private static OperatingSystem getOperatingSystem() { + switch (JmeSystem.getPlatform().getOs()) { + case Windows: return OperatingSystem.WINDOWS; + case MacOS: return OperatingSystem.MACOS; + case Linux: return OperatingSystem.LINUX; + case Android: return OperatingSystem.ANDROID; + case iOS: return OperatingSystem.IOS; + default: return OperatingSystem.UNKNOWN; + } + } - return base; - } - /** * Checks that the value is a single name which cannot escape a directory. */ @@ -580,7 +540,7 @@ public static synchronized String loadNativeLibrary(String name, boolean isRequi } UnsatisfiedLinkError error = new UnsatisfiedLinkError( "Cannot extract/load native libraries from the configured directory, temp, user cache, or ~/.jme3."); - while (extractionRootIndex < EXTRACTION_ROOT_COUNT) { + while (extractionCandidates == null || extractionRootIndex < extractionCandidates.size()) { Path target = null; boolean created = false; try { @@ -620,6 +580,7 @@ public static synchronized String loadNativeLibrary(String name, boolean isRequi } } extractionRootIndex = 0; + extractionCandidates = null; if (isRequired) { throw error; } diff --git a/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryExtractionTest.java b/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryExtractionTest.java index 6de6094bf2..d9ece84c19 100644 --- a/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryExtractionTest.java +++ b/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryExtractionTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; @@ -45,6 +46,9 @@ import java.nio.file.attribute.AclFileAttributeView; import java.nio.file.attribute.PosixFilePermissions; import java.util.Collections; +import java.util.Locale; +import com.jme3.nativebootstrap.common.OperatingSystem; +import com.jme3.nativebootstrap.directories.NativeDirectories; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -53,8 +57,8 @@ class NativeLibraryExtractionTest { @Test void createsUniquePrivateDirectories() throws Exception { - Path first = NativeLibraryExtraction.createDirectory(root, "native-"); - Path second = NativeLibraryExtraction.createDirectory(root, "native-"); + Path first = createDirectory(root); + Path second = createDirectory(root); assertNotEquals(first, second); if (Files.getFileStore(first).supportsFileAttributeView("posix")) { assertEquals(PosixFilePermissions.fromString("rwx------"), Files.getPosixFilePermissions(first)); @@ -75,10 +79,9 @@ void refusesSharedDirectoryWithoutStickyBit() throws Exception { Path shared = Files.createTempDirectory(publicTemp, "native-shared-test-"); try { Files.setPosixFilePermissions(shared, PosixFilePermissions.fromString("rwxrwxrwx")); - IOException failure = assertThrows(IOException.class, - () -> NativeLibraryExtraction.createDirectory(shared, "native-")); - assertEquals("Native extraction ancestor is writable by other users: " + shared.toRealPath(), - failure.getMessage()); + UncheckedIOException failure = assertThrows(UncheckedIOException.class, + () -> createDirectory(shared)); + assertTrue(failure.getCause().getMessage().contains(shared.toRealPath().toString())); } finally { try (DirectoryStream children = Files.newDirectoryStream(shared)) { for (Path child : children) Files.deleteIfExists(child); @@ -91,10 +94,16 @@ void refusesSharedDirectoryWithoutStickyBit() throws Exception { void refusesFilesystemsWithoutPrivatePermissions() throws Exception { URI zip = URI.create("jar:" + root.resolve("unsupported.zip").toUri()); try (FileSystem fs = FileSystems.newFileSystem(zip, Collections.singletonMap("create", "true"))) { - assertThrows(IOException.class, () -> NativeLibraryExtraction.createDirectory(fs.getPath("/"), "native-")); + assertThrows(UncheckedIOException.class, () -> createDirectory(fs.getPath("/"))); } } + private static Path createDirectory(Path root) { + return NativeDirectories.fromRoots("native", Collections.singletonList(root), + () -> OperatingSystem.valueOf(JmeSystem.getPlatform().getOs().name().toUpperCase(Locale.ROOT))) + .get(0).get(); + } + private static java.nio.file.attribute.UserPrincipal viewOwner(Path directory) { try { return directory.getFileSystem().getUserPrincipalLookupService() diff --git a/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderExtractionTest.java b/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderExtractionTest.java index 2cbd64e9f8..d771ee688e 100644 --- a/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderExtractionTest.java +++ b/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderExtractionTest.java @@ -55,6 +55,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.stream.Stream; @@ -71,7 +72,10 @@ class NativeLibraryLoaderExtractionTest { @BeforeEach void isolateLoaderState() throws Exception { + NativeLibraryLoader.isExtractNativeLibraries(); // Initialize the global namespace before saving properties. + properties.put("natives.namespace", System.getProperty("natives.namespace")); for (String key : new String[]{"java.io.tmpdir", "user.home", + "natives.tempDir", "natives.userHome", "natives.cacheDir", NativeLibraryLoader.CUSTOM_EXTRACTION_FOLDER_PROPERTY, NativeLibraryLoader.EXTRACT_NATIVE_LIBRARIES_PROPERTY, NativeLibraryLoader.CACHE_FOLDER_PROPERTY}) { @@ -79,7 +83,7 @@ void isolateLoaderState() throws Exception { System.clearProperty(key); } for (String key : new String[]{"extractionFolder", "extractionFolderOverride", - "extractNativeLibrariesOverride", "extractionRootIndex"}) { + "extractNativeLibrariesOverride", "extractionCandidates", "extractionRootIndex"}) { Field field = field(key); fields.put(key, field.get(null)); field.set(null, key.equals("extractionRootIndex") ? 0 : null); @@ -93,6 +97,9 @@ void isolateLoaderState() throws Exception { @AfterEach void restoreLoaderState() throws Exception { + System.clearProperty(NativeLibraryLoader.CUSTOM_EXTRACTION_FOLDER_PROPERTY); + System.clearProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY); + NativeLibraryLoader.setCustomExtractionFolder(null); for (Map.Entry entry : fields.entrySet()) field(entry.getKey()).set(null, entry.getValue()); for (Map.Entry entry : properties.entrySet()) { if (entry.getValue() == null) System.clearProperty(entry.getKey()); @@ -100,6 +107,45 @@ void restoreLoaderState() throws Exception { } } + @Test + void defaultNamespaceAlsoAppliesToOtherBootstrapLibraries() { + assertNull(System.getProperty("natives.namespace")); + List candidates = + com.jme3.nativebootstrap.directories.NativeDirectories.candidates("other-library", + () -> com.jme3.nativebootstrap.common.OperatingSystem.LINUX); + assertEquals(home.resolve(".jme3/natives/other-library"), + candidates.get(candidates.size() - 1).root()); + } + + @Test + void customFolderDoesNotOverwriteBootstrapTempOverride() throws Exception { + Path nativeTemp = Files.createDirectory(directory.resolve("bootstrap-temp")); + System.setProperty("natives.tempDir", nativeTemp.toString()); + Path custom = Files.createDirectory(directory.resolve("custom")); + NativeLibraryLoader.setCustomExtractionFolder(custom.toString()); + assertEquals(nativeTemp.toString(), System.getProperty("natives.tempDir")); + assertEquals(custom.toRealPath(), NativeLibraryLoader.getExtractionFolder().toPath().getParent()); + NativeLibraryLoader.setCustomExtractionFolder(null); + assertEquals(nativeTemp.toString(), System.getProperty("natives.tempDir")); + assertEquals(nativeTemp.toRealPath(), NativeLibraryLoader.getExtractionFolder().toPath().getParent()); + } + + @Test + void cachePropertyOnlyChangesWithJmeOverride() throws Exception { + Path nativeCache = Files.createDirectory(directory.resolve("bootstrap-cache")); + Path jmeCache = Files.createDirectory(directory.resolve("jme-cache")); + System.setProperty("natives.cacheDir", nativeCache.toString()); + System.clearProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY); + NativeLibraryLoader.setCustomExtractionFolder(null); + assertEquals(nativeCache.toString(), System.getProperty("natives.cacheDir")); + System.setProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY, jmeCache.toString()); + NativeLibraryLoader.getExtractionFolder(); + assertEquals(jmeCache.toString(), System.getProperty("natives.cacheDir")); + System.clearProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY); + NativeLibraryLoader.setCustomExtractionFolder(null); + assertEquals(jmeCache.toString(), System.getProperty("natives.cacheDir")); + } + @Test void tempIsPrivateAndDoesNotCreateHome() throws Exception { Path selected = NativeLibraryLoader.getExtractionFolder().toPath(); @@ -113,19 +159,48 @@ void tempIsPrivateAndDoesNotCreateHome() throws Exception { } @Test - void invalidHomeDoesNotBreakTemp() { - System.setProperty("user.home", "invalid" + (char) 0 + "home"); + void relativeHomeDoesNotBreakTemp() { + System.setProperty("user.home", "relative-home"); assertTrue(NativeLibraryLoader.getExtractionFolder().isDirectory()); } @Test - void invalidTempFallsBackToCache() throws Exception { + void relativeTempFallsBackToCache() throws Exception { Files.createDirectories(home.resolve(".cache")); - System.setProperty("java.io.tmpdir", "invalid" + (char) 0 + "temp"); + System.setProperty("java.io.tmpdir", "relative-temp"); Path selected = NativeLibraryLoader.getExtractionFolder().toPath(); assertEquals(cacheRoot().toRealPath(), selected.getParent()); } + @Test + void malformedDirectorySettingsReportRequiredAndOptionalLoadFailures() { + for (String key : new String[]{"user.home", "java.io.tmpdir"}) { + String previous = System.getProperty(key); + System.setProperty(key, "invalid" + (char) 0 + "path"); + String name = register("fixture.so", path -> fail("Must not load")); + UnsatisfiedLinkError failure = assertThrows(UnsatisfiedLinkError.class, + () -> NativeLibraryLoader.loadNativeLibrary(name, true)); + assertTrue(failure.getSuppressed()[0].getCause().getSuppressed()[0] + instanceof java.nio.file.InvalidPathException); + assertNull(NativeLibraryLoader.loadNativeLibrary(name, false)); + System.setProperty(key, previous); + } + } + + @Test + void configuredCacheReplacesPlatformCacheAfterTemp() throws Exception { + Path cache = Files.createDirectory(directory.resolve("configured-cache")); + System.setProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY, cache.toString()); + AtomicInteger attempts = new AtomicInteger(); + String loaded = NativeLibraryLoader.loadNativeLibrary(register("fixture.so", path -> { + if (attempts.getAndIncrement() == 0) throw new UnsatisfiedLinkError("temp failure"); + }), true); + assertEquals(2, attempts.get()); + assertEquals(cache.toString(), System.getProperty("natives.cacheDir")); + assertEquals(cache.resolve("jme3").toRealPath(), Paths.get(loaded).getParent().getParent()); + assertFalse(Files.exists(home)); + } + @Test void tempFileFallsBackToCacheDuringExtraction() throws Exception { Files.createDirectories(home.resolve(".cache")); @@ -144,7 +219,7 @@ void unusableTempAndCacheFallBackToHome() throws Exception { Files.createDirectories(cache.getParent()); Files.createFile(cache); String loaded = NativeLibraryLoader.loadNativeLibrary(register("fixture.so", path -> {}), true); - assertTrue(Paths.get(loaded).startsWith(home.resolve(".jme3").toRealPath())); + assertTrue(Paths.get(loaded).startsWith(home.resolve(".jme3/natives/jme3").toRealPath())); assertTrue(Files.isRegularFile(Paths.get(loaded))); } @@ -209,20 +284,64 @@ void errorsIncludeAllLoadAttemptsAndOptionalFailureReturnsNull() throws Exceptio void allUnusableRootsFailWithoutRetryingForever() throws Exception { Path notDirectory = Files.createFile(directory.resolve("not-a-directory")); Path cache = Files.createDirectory(directory.resolve("cache")); - Files.createFile(cache.resolve(".jme3")); + Files.createFile(cache.resolve("jme3")); NativeLibraryLoader.setCustomExtractionFolder(notDirectory.toString()); System.setProperty("java.io.tmpdir", notDirectory.toString()); System.setProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY, cache.toString()); - System.setProperty("user.home", directory.resolve("missing/home").toString()); + System.setProperty("user.home", Files.createFile(directory.resolve("home-file")).toString()); String name = register("fixture.so", path -> fail("Must not load")); UnsatisfiedLinkError error = assertThrows(UnsatisfiedLinkError.class, () -> NativeLibraryLoader.loadNativeLibrary(name, true)); assertEquals(1, error.getSuppressed().length); - assertEquals(4, error.getSuppressed()[0].getCause().getSuppressed().length); + assertEquals(3, error.getSuppressed()[0].getCause().getSuppressed().length); assertNull(NativeLibraryLoader.loadNativeLibrary(name, false)); } + @Test + void failedLoadRediscoversUpdatedCacheOverride() throws Exception { + System.setProperty("java.io.tmpdir", Files.createFile(directory.resolve("tmp-file")).toString()); + System.setProperty("user.home", Files.createFile(directory.resolve("home-file")).toString()); + AtomicBoolean denyLoad = new AtomicBoolean(true); + String name = register("fixture.so", path -> { + if (denyLoad.get()) throw new UnsatisfiedLinkError("mapping denied"); + }); + assertThrows(UnsatisfiedLinkError.class, () -> NativeLibraryLoader.loadNativeLibrary(name, true)); + + Path newCache = Files.createDirectory(directory.resolve("new-cache")); + System.setProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY, newCache.toString()); + denyLoad.set(false); + String loaded = NativeLibraryLoader.loadNativeLibrary(name, true); + assertEquals(newCache.resolve("jme3").toRealPath(), Paths.get(loaded).getParent().getParent()); + } + + @Test + void invalidLegacyCacheOverrideUsesBootstrapDefaults() throws Exception { + System.setProperty("java.io.tmpdir", Files.createFile(directory.resolve("tmp-file")).toString()); + Path missingCache = directory.resolve("missing-cache"); + Path cacheFile = Files.createFile(directory.resolve("cache-file")); + for (String override : new String[]{"relative-cache", missingCache.toString(), cacheFile.toString()}) { + System.setProperty(NativeLibraryLoader.CACHE_FOLDER_PROPERTY, override); + NativeLibraryLoader.setCustomExtractionFolder(null); + String loaded = NativeLibraryLoader.loadNativeLibrary(register("fixture.so", path -> {}), true); + assertTrue(Files.isRegularFile(Paths.get(loaded))); + assertFalse(Paths.get(loaded).startsWith(missingCache)); + } + assertFalse(Files.exists(missingCache)); + } + + @Test + void explicitBootstrapNamespaceIsPreserved() throws Exception { + System.setProperty("natives.namespace", ".custom-app"); + System.setProperty("java.io.tmpdir", Files.createFile(directory.resolve("tmp-file")).toString()); + Files.createDirectories(cacheRoot().getParent()); + Files.createFile(cacheRoot()); + String loaded = NativeLibraryLoader.loadNativeLibrary(register("fixture.so", path -> {}), true); + assertEquals(home.resolve(".custom-app/natives/jme3").toRealPath(), + Paths.get(loaded).getParent().getParent()); + assertEquals(".custom-app", System.getProperty("natives.namespace")); + } + @Test void fatalVmErrorIsNotSwallowed() { OutOfMemoryError fatal = new OutOfMemoryError("test sentinel"); @@ -290,6 +409,7 @@ void customDirectoryIsCreatedWhenExtracting() throws Exception { NativeLibraryLoader.setCustomExtractionFolder(custom.toString()); Path selected = NativeLibraryLoader.getExtractionFolder().toPath(); assertEquals(custom.toRealPath(), selected.getParent()); + assertNull(System.getProperty("natives.tempDir")); assertTrue(Files.isDirectory(selected)); String loaded = NativeLibraryLoader.loadNativeLibrary(register("fixture.so", path -> {}), true); @@ -355,7 +475,7 @@ private String register(String filename, Consumer loader) { } private Path cacheRoot() { - return home.resolve(".cache/.jme3"); + return home.resolve(".cache/jme3"); } private static Path createNoexecDirectory() throws Exception { diff --git a/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderTest.java b/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderTest.java index a3a116c04c..3b70a15d2d 100644 --- a/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderTest.java +++ b/jme3-desktop/src/test/java/com/jme3/system/NativeLibraryLoaderTest.java @@ -52,9 +52,14 @@ class NativeLibraryLoaderTest { private String previousExtractionFolder; private String previousExtractNativeLibraries; + private String previousNativeTempDir; + private String previousNativeCacheDir; @BeforeEach void rememberNativeLibrarySettings() { + NativeLibraryLoader.isExtractNativeLibraries(); + previousNativeTempDir = System.getProperty("natives.tempDir"); + previousNativeCacheDir = System.getProperty("natives.cacheDir"); previousExtractionFolder = System.getProperty(NativeLibraryLoader.CUSTOM_EXTRACTION_FOLDER_PROPERTY); previousExtractNativeLibraries = System.getProperty(NativeLibraryLoader.EXTRACT_NATIVE_LIBRARIES_PROPERTY); NativeLibraryLoader.setCustomExtractionFolder(null); @@ -63,6 +68,7 @@ void rememberNativeLibrarySettings() { @AfterEach void restoreNativeLibrarySettings() { + System.clearProperty(NativeLibraryLoader.CUSTOM_EXTRACTION_FOLDER_PROPERTY); NativeLibraryLoader.setCustomExtractionFolder(null); NativeLibraryLoader.clearExtractNativeLibrariesOverride(); if (previousExtractionFolder == null) { @@ -75,6 +81,10 @@ void restoreNativeLibrarySettings() { } else { System.setProperty(NativeLibraryLoader.EXTRACT_NATIVE_LIBRARIES_PROPERTY, previousExtractNativeLibraries); } + if (previousNativeTempDir == null) System.clearProperty("natives.tempDir"); + else System.setProperty("natives.tempDir", previousNativeTempDir); + if (previousNativeCacheDir == null) System.clearProperty("natives.cacheDir"); + else System.setProperty("natives.cacheDir", previousNativeCacheDir); } @Test