Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ dependencies {
compileOnlyApi(libs.jetbrains.annotations)
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

spotless {
java {
importOrderFile(rootProject.file(".spotless/vectrix.importorder"))
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ asm = "9.9.1"
checkstyle = "12.3.1"
indra = "4.0.0"
tinylog = "2.7.0"
junit = "5.13.4"

[libraries]
jetbrains-annotations = { module = "org.jetbrains:annotations", version = "26.1.0" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
junit-launcher = { module = "org.junit.platform:junit-platform-launcher" }

# logger
tinylog-api = { module = "org.tinylog:tinylog-api", version.ref = "tinylog" }
Expand Down
4 changes: 4 additions & 0 deletions launcher/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ dependencies {
implementation(libs.asm.util)

implementation(libs.gson)

testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.launcher)
}
53 changes: 21 additions & 32 deletions launcher/src/main/java/space/vectrix/ignite/util/ClassLoaders.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
*/
package space.vectrix.ignite.util;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.tinylog.Logger;

Expand All @@ -45,47 +48,33 @@ public final class ClassLoaders {
* @return the system class path urls
* @since 1.0.0
*/
@SuppressWarnings({"restriction", "unchecked"})
public static URL@NotNull [] systemClassPaths() {
final ClassLoader classLoader = ClassLoaders.class.getClassLoader();
if(classLoader instanceof URLClassLoader) {
return ((URLClassLoader) classLoader).getURLs();
}

if(classLoader.getClass().getName().startsWith("jdk.internal.loader.ClassLoaders$")) {
try {
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
final Method objectFieldOffsetMethod = unsafeClass.getDeclaredMethod("objectFieldOffset", Field.class);
final Method getObjectMethod = unsafeClass.getDeclaredMethod("getObject", Object.class, long.class);

final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
final Object unsafe = unsafeField.get(null);

// jdk.internal.loader.ClassLoaders.AppClassLoader.ucp
Field ucpField;
try {
ucpField = classLoader.getClass().getDeclaredField("ucp");
} catch(final NoSuchFieldException | SecurityException e) {
ucpField = classLoader.getClass().getSuperclass().getDeclaredField("ucp");
}

final long ucpFieldOffset = (long) objectFieldOffsetMethod.invoke(unsafe, ucpField);
final Object ucpObject = getObjectMethod.invoke(unsafe, classLoader, ucpFieldOffset);
final String classPath = System.getProperty("java.class.path", "");
if(classPath.isEmpty()) {
return new URL[0];
}

// jdk.internal.loader.URLClassPath.path
final Field pathField = ucpField.getType().getDeclaredField("path");
final long pathFieldOffset = (long) objectFieldOffsetMethod.invoke(unsafe, pathField);
final ArrayList<URL> path = (ArrayList<URL>) getObjectMethod.invoke(unsafe, ucpObject, pathFieldOffset);
final String separator = System.getProperty("path.separator", File.pathSeparator);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just using File.pathSeparator would be good enough, don't need to grab it from the system property.

final List<URL> urls = new ArrayList<>();
for(final String entry : classPath.split(java.util.regex.Pattern.quote(separator))) {

@vectrixdevelops vectrixdevelops Mar 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not use regex here. We can simply do split(File.pathSeparator).

if(entry.isEmpty()) {
continue;
}

return path.toArray(new URL[0]);
} catch(final Exception exception) {
Logger.error(exception, "Failed to retrieve system classloader paths!");
return new URL[0];
try {
final Path path = Paths.get(entry);
urls.add(path.toUri().toURL());
} catch(final MalformedURLException exception) {
Logger.warn(exception, "Skipping malformed class path entry: {}", entry);
}
}

return new URL[0];
return urls.toArray(new URL[0]);
}

private ClassLoaders() {
Expand Down
127 changes: 127 additions & 0 deletions launcher/src/test/java/space/vectrix/ignite/util/ClassLoadersTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* This file is part of Ignite, licensed under the MIT License (MIT).
*
* Copyright (c) vectrix.space <https://vectrix.space/>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package space.vectrix.ignite.util;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;

class ClassLoadersTest {
@Test
void systemClassPathsMatchesJavaClassPathProperty() {
final URL[] expected = this.classPathPropertyUrls();
final URL[] actual = ClassLoaders.systemClassPaths();

assertArrayEquals(expected, actual);
}

@Test
void systemClassPathsMatchesLegacyUnsafeLookupOnJdkInternalClassLoader() throws Exception {
final ClassLoader classLoader = ClassLoaders.class.getClassLoader();
if(classLoader instanceof URLClassLoader || !classLoader.getClass().getName().startsWith("jdk.internal.loader.ClassLoaders$")) {
return;
}

final URL[] expected = this.legacyUnsafeSystemClassPaths(classLoader);
final URL[] actual = ClassLoaders.systemClassPaths();

assertArrayEquals(expected, actual);
}

@Test
void javaClassPathPropertyExpansionIsStable() {
final String classPath = System.getProperty("java.class.path", "");
final String separator = System.getProperty("path.separator", File.pathSeparator);

final long nonEmptyEntries = classPath.isEmpty()
? 0
: java.util.regex.Pattern.compile(java.util.regex.Pattern.quote(separator))
.splitAsStream(classPath)
.filter(entry -> !entry.isEmpty())
.count();

assertEquals(nonEmptyEntries, ClassLoaders.systemClassPaths().length);
}

private URL[] classPathPropertyUrls() {
final String classPath = System.getProperty("java.class.path", "");
if(classPath.isEmpty()) {
return new URL[0];
}

final String separator = System.getProperty("path.separator", File.pathSeparator);
final List<URL> urls = new ArrayList<>();
for(final String entry : classPath.split(java.util.regex.Pattern.quote(separator))) {
if(entry.isEmpty()) {
continue;
}

final Path path = Paths.get(entry);
try {
urls.add(path.toUri().toURL());
} catch(final Exception exception) {
throw new AssertionError("Failed to convert class path entry to URL: " + entry, exception);
}
}

return urls.toArray(new URL[0]);
}

@SuppressWarnings({"restriction", "unchecked"})
private URL[] legacyUnsafeSystemClassPaths(final ClassLoader classLoader) throws Exception {
final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
final Method objectFieldOffsetMethod = unsafeClass.getDeclaredMethod("objectFieldOffset", Field.class);
final Method getObjectMethod = unsafeClass.getDeclaredMethod("getObject", Object.class, long.class);

final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
final Object unsafe = unsafeField.get(null);

Field ucpField;
try {
ucpField = classLoader.getClass().getDeclaredField("ucp");
} catch(final NoSuchFieldException | SecurityException exception) {
ucpField = classLoader.getClass().getSuperclass().getDeclaredField("ucp");
}

final long ucpFieldOffset = (long) objectFieldOffsetMethod.invoke(unsafe, ucpField);
final Object ucpObject = getObjectMethod.invoke(unsafe, classLoader, ucpFieldOffset);

final Field pathField = ucpField.getType().getDeclaredField("path");
final long pathFieldOffset = (long) objectFieldOffsetMethod.invoke(unsafe, pathField);
final ArrayList<URL> path = (ArrayList<URL>) getObjectMethod.invoke(unsafe, ucpObject, pathFieldOffset);
return path.toArray(new URL[0]);
}
}