From 2b597974f24e0b8f355c5381196d782faeb12a34 Mon Sep 17 00:00:00 2001 From: AlexIIL Date: Sat, 7 Dec 2024 23:31:18 +0000 Subject: [PATCH] Fix the resource scanner in KnotClassLoader not respecting the addition order, at least for quilt's own additions. This is implemented by handling jar files ourselves in KnotClassLoader.addPath, rather than delegating directly to URLClassLoader. This fixes an issue when mods include incompatible versions of an existing library that minecraft ships, causing later crashes. This doesn't handle URLs added through addURL yet - those will always be added to the end of the search order, even if other paths are added later. --- gradle.properties | 2 +- .../loader/api/plugin/QuiltPluginManager.java | 20 ++++- .../quiltmc/loader/impl/QuiltLoaderImpl.java | 2 +- .../impl/filesystem/QuiltZipFileSystem.java | 85 ++++++++++++++++++- .../impl/launch/knot/KnotClassLoader.java | 19 +++-- .../impl/plugin/QuiltPluginManagerImpl.java | 21 ++++- .../quiltmc/loader/impl/util/FileUtil.java | 1 + .../loader/impl/util/JavaVersionUtil.java | 50 +++++++++++ .../impl/util/MultiReleaseJarCandidate.java | 34 ++++++++ 9 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/quiltmc/loader/impl/util/JavaVersionUtil.java create mode 100644 src/main/java/org/quiltmc/loader/impl/util/MultiReleaseJarCandidate.java diff --git a/gradle.properties b/gradle.properties index e4b24df6a..bb8d01d80 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ group = org.quiltmc description = The mod loading component of Quilt url = https://github.com/quiltmc/quilt-loader # Don't forget to change this in QuiltLoaderImpl as well -quilt_loader = 0.28.0-beta.1 +quilt_loader = 0.28.0-beta.2 # Fabric & Quilt Libraries asm = 9.7.1 diff --git a/src/main/java/org/quiltmc/loader/api/plugin/QuiltPluginManager.java b/src/main/java/org/quiltmc/loader/api/plugin/QuiltPluginManager.java index b2a131c8e..8bacb3e79 100644 --- a/src/main/java/org/quiltmc/loader/api/plugin/QuiltPluginManager.java +++ b/src/main/java/org/quiltmc/loader/api/plugin/QuiltPluginManager.java @@ -65,9 +65,27 @@ public interface QuiltPluginManager { * * @throws IOException if something went wrong while loading the file. * @throws NonZipException if {@link FileSystems#newFileSystem(Path, ClassLoader)} throws a - * {@link ProviderNotFoundException}. */ + * {@link ProviderNotFoundException}. + * @see #loadJarNow(Path) */ Path loadZipNow(Path zip) throws IOException, NonZipException; + /** Loads the specified zip file as a jar file and returns a path to the root of it's contents. Unlike + * {@link #loadZipNow(Path)} this has special handling for multi-release jars, and this will expose the correct + * multi-release files in the root of the returned zips filesystem. This may throw an exception if the manifest is + * malformed. This does not check signatures, index files, or anything else. + *

+ * How the given zip is loaded depends on loaders config settings - in particular the zip could be extracted to a + * temporary folder on the same filesystem as the original zip. + *

+ * WARNING: if this method allocates a new {@link FileSystem} then that will be closed, unless at least one + * of the {@link QuiltLoaderPlugin}s {@link QuiltPluginContext#lockZip(Path) locks} it, or if a chosen mod is loaded + * from it. + * + * @throws IOException if something went wrong while loading the file. + * @throws NonZipException if {@link FileSystems#newFileSystem(Path, ClassLoader)} throws a + * {@link ProviderNotFoundException}. */ + Path loadJarNow(Path zip) throws IOException, NonZipException; + /** Creates a new in-memory read-write file system. This can be used for mods that aren't loaded from zips. * * @return The root {@link Path} of the newly allocated {@link FileSystem} */ diff --git a/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java b/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java index bfb5a0929..4c38e586a 100644 --- a/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java +++ b/src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java @@ -131,7 +131,7 @@ public final class QuiltLoaderImpl { public static final int ASM_VERSION = Opcodes.ASM9; - public static final String VERSION = "0.28.0-beta.1"; + public static final String VERSION = "0.28.0-beta.2"; public static final String MOD_ID = "quilt_loader"; public static final String DEFAULT_MODS_DIR = "mods"; public static final String DEFAULT_CACHE_DIR = ".cache"; diff --git a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java index b591a3910..ecc6a239c 100644 --- a/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java +++ b/src/main/java/org/quiltmc/loader/impl/filesystem/QuiltZipFileSystem.java @@ -41,6 +41,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.Attributes; +import java.util.jar.Manifest; import java.util.zip.GZIPInputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -48,13 +50,14 @@ import java.util.zip.ZipInputStream; import org.jetbrains.annotations.Nullable; -import org.quiltmc.loader.api.plugin.NonZipException; import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFile; +import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolder; import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderReadOnly; import org.quiltmc.loader.impl.filesystem.QuiltUnifiedEntry.QuiltUnifiedFolderWriteable; import org.quiltmc.loader.impl.util.DisconnectableByteChannel; import org.quiltmc.loader.impl.util.ExposedByteArrayOutputStream; import org.quiltmc.loader.impl.util.FileUtil; +import org.quiltmc.loader.impl.util.JavaVersionUtil; import org.quiltmc.loader.impl.util.LimitedInputStream; import org.quiltmc.loader.impl.util.QuiltLoaderCleanupTasks; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; @@ -77,7 +80,17 @@ public class QuiltZipFileSystem extends QuiltMapFileSystem thisRef = new WeakReference<>(this); final ZipSource source; + @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) + public enum ZipHandling { + PLAIN, + JAR; + } + public QuiltZipFileSystem(String name, Path zipFrom, String zipPathPrefix) throws IOException { + this(name, zipFrom, zipPathPrefix, ZipHandling.PLAIN); + } + + public QuiltZipFileSystem(String name, Path zipFrom, String zipPathPrefix, ZipHandling zip) throws IOException { super(QuiltZipFileSystem.class, QuiltZipPath.class, name, true); if (DEBUG_TEST_READING) { @@ -121,6 +134,10 @@ public QuiltZipFileSystem(String name, Path zipFrom, String zipPathPrefix) throw source.build(); + if (zip == ZipHandling.JAR) { + setupMultiReleaseJar(); + } + switchToReadOnly(); QuiltZipFileSystemProvider.PROVIDER.register(this); @@ -133,6 +150,72 @@ protected boolean startWithConcurrentMap() { return false; } + + private void setupMultiReleaseJar() throws IOException { + + int javaVersion = JavaVersionUtil.getJavaVersion(); + if (javaVersion < 9) { + return; + } + + Path metaInf = getRoot().resolve("META-INF"); + if (!exists(metaInf)) { + return; + } + Path versionsPath = metaInf.resolve("versions"); + if (!exists(versionsPath)) { + return; + } + Path manifestPath = metaInf.resolve("MANIFEST.MF"); + if (!exists(manifestPath)) { + return; + } + + try (InputStream manifestStream = Files.newInputStream(manifestPath)) { + Manifest manifest = new Manifest(manifestStream); + String multiReleaseValue = manifest.getMainAttributes().getValue("Multi-Release"); + if (!"true".equalsIgnoreCase(multiReleaseValue)) { + return; + } + } + + for (int version = 9; version <= javaVersion; version++) { + Path exactVersionPath = versionsPath.resolve(Integer.toString(version)); + if (!exists(exactVersionPath)) { + continue; + } + + if (!isDirectory(exactVersionPath)) { + continue; + } + + copyMultiReleaseEntry(exactVersionPath, root); + } + } + + private void copyMultiReleaseEntry(Path from, QuiltZipPath to) throws IOException { + QuiltUnifiedEntry entry = getEntry(from); + if (entry instanceof QuiltUnifiedFolder) { + QuiltUnifiedFolder folder = (QuiltUnifiedFolder) entry; + String folderName = folder.path.name; + if (to == root && "META-INF".equals(folderName)) { + return; + } + + if (!isDirectory(to)) { + addEntryRequiringParent(new QuiltUnifiedFolderWriteable(to)); + } + + for (Path child : folder.getChildren()) { + copyMultiReleaseEntry(child, to.resolve(child.getFileName())); + } + } else { + QuiltUnifiedFile file = (QuiltUnifiedFile) entry; + removeEntry(to, false); + addEntryRequiringParent(file.createMovedTo(to)); + } + } + private void initializeFromZip(InputStream fileStream, String zipPathPrefix) throws IOException { try (CountingInputStream counter = new CountingInputStream(fileStream); // CustomZipInputStream zip = new CustomZipInputStream(counter)// diff --git a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassLoader.java b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassLoader.java index 0f797d023..702e68bf0 100644 --- a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassLoader.java +++ b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassLoader.java @@ -20,13 +20,13 @@ import net.fabricmc.api.EnvType; import org.quiltmc.loader.api.ModContainer; -import org.quiltmc.loader.api.QuiltLoader; import org.quiltmc.loader.impl.filesystem.QuiltClassPath; +import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem.ZipHandling; import org.quiltmc.loader.impl.game.GameProvider; import org.quiltmc.loader.impl.util.DeferredInputStream; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; -import org.quiltmc.loader.impl.util.SystemProperties; import org.quiltmc.loader.impl.util.UrlUtil; import org.quiltmc.loader.impl.util.log.Log; import org.quiltmc.loader.impl.util.log.LogCategory; @@ -44,7 +44,6 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.Optional; @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) class KnotClassLoader extends SecureClassLoader implements KnotClassLoaderInterface { @@ -283,8 +282,18 @@ public void addPath(Path root, ModContainer mod, URL origin) { delegate.setMod(root, asUrl, mod); fakeLoader.addURL(asUrl); if (root.getFileName() != null && root.getFileName().toString().endsWith(".jar")) { - // TODO: Perhaps open it in a more efficient manor? - minimalLoader.addURL(asUrl); + Path zipRoot = null; + try { + String fsName = "classpath-" + root.getFileName().toString(); + zipRoot = new QuiltZipFileSystem(fsName, root, "", ZipHandling.JAR).getRoot(); + } catch (IOException io) { + Log.warn(LogCategory.GENERAL, "Failed to open the file " + root + ", adding it via the slow method instead!", io); + } + if (zipRoot != null) { + paths.addRoot(zipRoot); + } else { + minimalLoader.addURL(asUrl); + } } else { paths.addRoot(root); } diff --git a/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java b/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java index 134b45744..702b11f79 100644 --- a/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java +++ b/src/main/java/org/quiltmc/loader/impl/plugin/QuiltPluginManagerImpl.java @@ -94,6 +94,7 @@ import org.quiltmc.loader.impl.filesystem.QuiltJoinedPath; import org.quiltmc.loader.impl.filesystem.QuiltMemoryFileSystem; import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem; +import org.quiltmc.loader.impl.filesystem.QuiltZipFileSystem.ZipHandling; import org.quiltmc.loader.impl.filesystem.QuiltZipPath; import org.quiltmc.loader.impl.filesystem.ZeroByteFileException; import org.quiltmc.loader.impl.game.GameProvider; @@ -245,9 +246,18 @@ private Path loadZip0(Path zip) throws IOException, NonZipException { @Override public Path loadZipNow(Path zip) throws IOException, NonZipException { + return loadZipLikeNow(zip, ZipHandling.PLAIN); + } + + @Override + public Path loadJarNow(Path zip) throws IOException, NonZipException { + return loadZipLikeNow(zip, ZipHandling.JAR); + } + + private Path loadZipLikeNow(Path zip, ZipHandling handling) throws IOException, NonZipException { String name = zip.getFileName().toString(); try { - QuiltZipPath qRoot = new QuiltZipFileSystem(name, zip, "").getRoot(); + QuiltZipPath qRoot = new QuiltZipFileSystem(name, zip, "", handling).getRoot(); pathParents.put(qRoot, zip); return qRoot; } catch (IOException e) { @@ -256,7 +266,14 @@ public Path loadZipNow(Path zip) throws IOException, NonZipException { throw e; } // Something probably went wrong while trying to load them as zips - throw new IOException("Failed to read " + zip + " as a zip file: " + e.getMessage(), e); + StringBuilder msg = new StringBuilder(); + msg.append("Failed to read "); + msg.append(zip); + msg.append(" as a "); + msg.append(handling == ZipHandling.JAR ? "jar": "zip"); + msg.append(" file: "); + msg.append(e.getMessage()); + throw new IOException(msg.toString(), e); } else { throw new NonZipException(e); } diff --git a/src/main/java/org/quiltmc/loader/impl/util/FileUtil.java b/src/main/java/org/quiltmc/loader/impl/util/FileUtil.java index b071831cd..4cacb92a5 100644 --- a/src/main/java/org/quiltmc/loader/impl/util/FileUtil.java +++ b/src/main/java/org/quiltmc/loader/impl/util/FileUtil.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStream; +@MultiReleaseJarCandidate @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) public final class FileUtil { diff --git a/src/main/java/org/quiltmc/loader/impl/util/JavaVersionUtil.java b/src/main/java/org/quiltmc/loader/impl/util/JavaVersionUtil.java new file mode 100644 index 000000000..e4b73e5e2 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/util/JavaVersionUtil.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.impl.util; + +@MultiReleaseJarCandidate +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public final class JavaVersionUtil { + + private static int JAVA_VERSION = -1; + + public static int getJavaVersion() { + if (JAVA_VERSION < 0) { + String jVersion = System.getProperty("java.version", ""); + if (jVersion.startsWith("1.")) { + // Java 8 or earlier + // However loader itself requires java 8, so just force java 8 + JAVA_VERSION = 8; + } else { + int firstDot = jVersion.indexOf('.'); + if (firstDot > 0) { + try { + JAVA_VERSION = Integer.parseInt(jVersion.substring(0, firstDot)); + } catch (NumberFormatException nfe) { + throw new IllegalStateException( + "Unable to convert 'java.version' (" + jVersion + ") into a version number!", nfe + ); + } + } else { + throw new IllegalStateException("Unable to convert 'java.version' (" + jVersion + ") into a version number!"); + } + } + } + + return JAVA_VERSION; + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/util/MultiReleaseJarCandidate.java b/src/main/java/org/quiltmc/loader/impl/util/MultiReleaseJarCandidate.java new file mode 100644 index 000000000..5a697c746 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/util/MultiReleaseJarCandidate.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 QuiltMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.loader.impl.util; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** Indicates to future quilt maintainers that the class is intended to be converted to a Multi-Release jar after we add + * support for it, and then this annotation should be deleted. + *

+ * (This merely allows for this to be searched) */ +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +@Retention(SOURCE) +@Target(TYPE) +public @interface MultiReleaseJarCandidate { + +}