Skip to content

Commit

Permalink
Fix the resource scanner in KnotClassLoader not respecting the additi…
Browse files Browse the repository at this point in the history
…on 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.
  • Loading branch information
AlexIIL committed Dec 7, 2024
1 parent 7a2754a commit 2b59797
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 11 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* <p>
* WARNING: if this method allocates a new {@link FileSystem} then that will be closed, <em>unless</em> 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} */
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/quiltmc/loader/impl/QuiltLoaderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,23 @@
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;
import java.util.zip.ZipEntry;
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;
Expand All @@ -77,7 +80,17 @@ public class QuiltZipFileSystem extends QuiltMapFileSystem<QuiltZipFileSystem, Q
final WeakReference<QuiltZipFileSystem> 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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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)//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/quiltmc/loader/impl/util/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.InputStream;

@MultiReleaseJarCandidate
@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL)
public final class FileUtil {

Expand Down
50 changes: 50 additions & 0 deletions src/main/java/org/quiltmc/loader/impl/util/JavaVersionUtil.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* (This merely allows for this to be searched) */
@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL)
@Retention(SOURCE)
@Target(TYPE)
public @interface MultiReleaseJarCandidate {

}

0 comments on commit 2b59797

Please sign in to comment.