package de.geolykt.starloader.launcher; import java.io.IOException; import java.io.InputStream; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; import org.spongepowered.asm.launch.MixinBootstrap; import org.spongepowered.asm.mixin.Mixins; import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import de.geolykt.starloader.mod.ExtensionPrototype; /** * Collection of static utility methods. */ public final class Utils { private static final byte[] SHARED_DUMMY_ARRAY = new byte[4096]; private static final boolean LOCAL_LOGS = Boolean.getBoolean("org.stianloader.launcher.Utils.localLogs"); /** * Expand placeholders as defined by {@link ExtensionPrototype#getDefinedProperties()}. * This method will recursively replace placeholders. * * @param sourceResource The resource from which the input string comes from, used when logging unknown keys. Logged as-is, * so the {@link Object#toString()} method of the value passed as this argument should be human-readable. * @param string The input string which may contain placeholders to expand. * @param startIndex The index from which the expansion should start from. When calling this method directly, * it usually can be set to 0 to replace the entire string. * @param placeholders The placeholders to apply. * @return The returned string with all relevant properties being expanded. * @since 4.0.0-a20240711 */ @NotNull @ApiStatus.AvailableSince("4.0.0-a20240711") @Contract(pure = true) public static final String applyPlaceholders(@NotNull Object sourceResource, @NotNull String string, int startIndex, @NotNull Map placeholders) { int indexStart = string.indexOf("${", startIndex); if (indexStart == -1) { return string; } int indexEnd = string.indexOf('}', indexStart); String property = string.substring(indexStart + 2, indexEnd); String replacement = placeholders.get(property); if (replacement == null) { LoggerFactory.getLogger(Utils.class).warn("Could not expand resource '{}': Unknown property (or a null value for said property): '{}'", sourceResource, property); return Utils.applyPlaceholders(sourceResource, string, indexEnd, placeholders); } string = string.substring(0, indexStart) + replacement + string.substring(indexEnd + 1); return Utils.applyPlaceholders(sourceResource, string, indexStart, placeholders); } public static final void fastExhaust(@NotNull InputStream in) throws IOException { while (in.read(Utils.SHARED_DUMMY_ARRAY) != -1); } /** * Obtains the directory where SLL puts its logs. * *

It will first try to set the log directory to the value described by XDG_STATE_HOME, then * "%APPDATA%/stianloader", then "${user.home}/.local/state/stianloader" and if it still fails * it will put it in the current working directory. It will however only create a single directory * in order to accomplish this, so it won't always use the current working directory * if less advanced users attempt to use the launcher. * * @return The directory in which the logs of SLL are stored. */ public static final Path getLogDirectory() { if (Utils.LOCAL_LOGS) { Path logDir = Paths.get("logs"); try { if (Files.notExists(logDir)) { Files.createDirectory(logDir); } } catch (IOException e) { e.printStackTrace(); } return logDir; } // XDG base directory specification: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html String xdgStateHome = System.getenv("XDG_STATE_HOME"); if (xdgStateHome != null && !xdgStateHome.isEmpty()) { Path p = Paths.get(xdgStateHome, "stianloader"); try { Files.createDirectories(p); return p; } catch (IOException e) { e.printStackTrace(); } } String appdataFolder = System.getenv("APPDATA"); if (appdataFolder != null) { Path location = Paths.get(appdataFolder, "stianloader"); try { if (!Files.isDirectory(location)) { Files.createDirectory(location); } } catch (IOException expected) { expected.printStackTrace(); return Paths.get("."); } return location; } else { String userhome = System.getProperty("user.home"); if (userhome == null) { return Paths.get("."); } Path location = Paths.get(userhome, ".local"); if (Files.notExists(location)) { return Paths.get("."); } location = location.resolve("state"); if (Files.notExists(location)) { try { if (!Files.isDirectory(location)) { Files.createDirectory(location); } } catch (IOException expected) { expected.printStackTrace(); return Paths.get("."); } } location = location.resolve("stianloader"); try { if (!Files.isDirectory(location)) { Files.createDirectory(location); } } catch (IOException expected) { expected.printStackTrace(); return Paths.get("."); } return location; } } public static final void startMain(@NotNull Class className, String[] args) { try { MethodHandles.publicLookup().findStatic(className, "main", MethodType.methodType(void.class, String[].class)).invokeExact(args); } catch (Throwable e) { throw new RuntimeException("Error while invoking main class!", e); } } @SuppressWarnings("resource") protected static final void startMixin(String[] args) { MixinBootstrap.init(); MixinBootstrap.getPlatform().inject(); Mixins.getConfigs().forEach(c -> MinestomRootClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage())); } @Nullable public static final URI toCodeSourceURI(@Nullable URL url, @NotNull String internalClassName) { if (url == null) { return null; } String urlPath = url.getPath(); if (urlPath.endsWith(".class")) { String urlProtocol = url.getProtocol(); if (urlProtocol.equals("jar")) { int index0 = urlPath.indexOf('!'); if (index0 >= 0) { try { return new URI(urlPath.substring(0, index0)); } catch (URISyntaxException e) { LoggerFactory.getLogger(Utils.class).warn("Unable to assimilate jar-protocol-URL: '{}'", url, e); } } } else if (urlProtocol.equals("file")) { String expectedSuffix = internalClassName.replace('.', '/') + ".class"; if (urlPath.endsWith(expectedSuffix)) { try { return new URI("file", null, url.getHost(), url.getPort(), urlPath.substring(0, urlPath.length() - expectedSuffix.length()), null, null); } catch (URISyntaxException e) { LoggerFactory.getLogger(Utils.class).warn("Unable to assimilate file-protocol-URL: '{}'", url, e); } } } } try { return url.toURI(); } catch (URISyntaxException e) { LoggerFactory.getLogger(Utils.class).debug("Cannot convert URL {} to a URI.", url, e); return null; } } private Utils() {} }