diff --git a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java index 572855006..6b7f8af2b 100644 --- a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java +++ b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/MinecraftGameProvider.java @@ -176,15 +176,15 @@ public boolean locateGame(FabricLauncher launcher, String[] args) { if (commonGameJarDeclared) { if (envGameJar != null) { - classifier.process(envGameJar, McLibrary.MC_COMMON); + classifier.process(envGameJar, true, McLibrary.MC_COMMON); } - classifier.process(commonGameJar); + classifier.process(commonGameJar, true); } else if (envGameJar != null) { - classifier.process(envGameJar); + classifier.process(envGameJar, true); } - classifier.process(launcher.getClassPath()); + classifier.process(launcher.getClassPath(), false); if (classifier.has(McLibrary.MC_BUNDLER)) { BundlerProcessor.process(classifier); diff --git a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java index 44ca87194..d790aa513 100644 --- a/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java +++ b/minecraft/src/main/java/net/fabricmc/loader/impl/game/minecraft/launchwrapper/FabricTweaker.java @@ -54,7 +54,6 @@ import net.fabricmc.loader.impl.launch.FabricMixinBootstrap; import net.fabricmc.loader.impl.util.Arguments; import net.fabricmc.loader.impl.util.FileSystemUtil; -import net.fabricmc.loader.impl.util.LoaderUtil; import net.fabricmc.loader.impl.util.ManifestUtil; import net.fabricmc.loader.impl.util.SystemProperties; import net.fabricmc.loader.impl.util.UrlUtil; @@ -123,15 +122,14 @@ public void injectIntoClassLoader(LaunchClassLoader launchClassLoader) { private void init() { setupUncaughtExceptionHandler(); - classPath.clear(); + List paths = new ArrayList<>(); for (URL url : launchClassLoader.getSources()) { - Path path = UrlUtil.asPath(url); - if (!Files.exists(path)) continue; - - classPath.add(LoaderUtil.normalizeExistingPath(path)); + paths.add(UrlUtil.asPath(url)); } + classPath.clear(); + classPath.addAll(normalizeClassPath(paths)); GameProvider provider = new MinecraftGameProvider(); if (!provider.isEnabled() diff --git a/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java b/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java index ce051e500..62c4bf00c 100644 --- a/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java +++ b/src/main/java/net/fabricmc/loader/impl/game/LibClassifier.java @@ -32,9 +32,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.jar.JarFile; import java.util.jar.Manifest; -import java.util.zip.ZipEntry; import java.util.zip.ZipError; import java.util.zip.ZipFile; @@ -135,10 +133,8 @@ private void processManifestClassPath(LoaderLibrary lib, EnvType env) throws IOE Manifest manifest; try (ZipFile zf = new ZipFile(lib.path.toFile())) { - ZipEntry entry = zf.getEntry(JarFile.MANIFEST_NAME); - if (entry == null) return; - - manifest = new Manifest(zf.getInputStream(entry)); + manifest = ManifestUtil.readManifest(zf); + if (manifest == null) return; } List cp = ManifestUtil.getClassPath(manifest, lib.path); @@ -150,21 +146,21 @@ private void processManifestClassPath(LoaderLibrary lib, EnvType env) throws IOE } public void process(URL url) throws IOException { - process(UrlUtil.asPath(url)); + process(UrlUtil.asPath(url), true); } @SafeVarargs - public final void process(Iterable paths, L... excludedLibs) throws IOException { + public final void process(Iterable paths, boolean normalize, L... excludedLibs) throws IOException { Set excluded = makeSet(excludedLibs); for (Path path : paths) { - process(path, excluded); + process(path, normalize, excluded); } } @SafeVarargs - public final void process(Path path, L... excludedLibs) throws IOException { - process(path, makeSet(excludedLibs)); + public final void process(Path path, boolean normalize, L... excludedLibs) throws IOException { + process(path, normalize, makeSet(excludedLibs)); } private static > Set makeSet(L[] libs) { @@ -179,8 +175,8 @@ private static > Set makeSet(L[] libs) { return ret; } - private void process(Path path, Set excludedLibs) throws IOException { - path = LoaderUtil.normalizeExistingPath(path); + private void process(Path path, boolean normalize, Set excludedLibs) throws IOException { + if (normalize) path = LoaderUtil.normalizeExistingPath(path); if (systemLibraries.contains(path)) return; boolean matched = false; diff --git a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java index 856595331..50d0ba39b 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/FabricLauncherBase.java @@ -20,7 +20,22 @@ import java.io.FileOutputStream; import java.io.PrintWriter; import java.lang.reflect.Method; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipError; +import java.util.zip.ZipFile; import org.spongepowered.asm.mixin.MixinEnvironment; @@ -28,6 +43,9 @@ import net.fabricmc.loader.impl.FormattedException; import net.fabricmc.loader.impl.game.GameProvider; import net.fabricmc.loader.impl.gui.FabricGuiEntry; +import net.fabricmc.loader.impl.util.LoaderUtil; +import net.fabricmc.loader.impl.util.ManifestUtil; +import net.fabricmc.loader.impl.util.UrlUtil; import net.fabricmc.loader.impl.util.log.Log; import net.fabricmc.loader.impl.util.log.LogCategory; @@ -143,4 +161,76 @@ protected static void finishMixinBootstrapping() { public static boolean isMixinReady() { return mixinReady; } + + /** + * Normalize the class path by normalizing the paths, deduplicating, resolving/expanding indirect references and + * suppressing purely other-jar referencing jars. + */ + protected static List normalizeClassPath(List cp) { + Set checkedPaths = new HashSet<>(cp.size()); + List ret = new ArrayList<>(cp.size()); + List missing = new ArrayList<>(); + Deque queue = new ArrayDeque<>(cp); + Path path; + + while ((path = queue.pollFirst()) != null) { + if (!Files.exists(path)) { + missing.add(path); + continue; + } + + path = LoaderUtil.normalizeExistingPath(path); + if (!checkedPaths.add(path)) continue; + + if (Files.isRegularFile(path)) { // jar, expand (resolve additional manifest-referenced cp entries) and suppress file if it is only such a referencing container + try (ZipFile zf = new ZipFile(path.toFile())) { + Manifest manifest = ManifestUtil.readManifest(zf); + List urls; + + if (manifest != null + && (urls = ManifestUtil.getClassPath(manifest, path)) != null) { + for (int i = urls.size() - 1; i >= 0; i--) { + queue.addFirst(UrlUtil.asPath(urls.get(i))); + } + + // check for non-manifest/signature files to determine whether the jar can be ignored + + final String prefix = ManifestUtil.MANIFEST_DIR+"/"; + boolean foundNonManifest = false; + + for (Enumeration e = zf.entries(); e.hasMoreElements(); ) { + ZipEntry entry = e.nextElement(); + if (entry.isDirectory()) continue; + + String name = entry.getName(); + String fileName; + + if (!name.startsWith(prefix) // file outside META-INF + || (fileName = name.substring(prefix.length())).indexOf('/') >= 0 // file not directly within META-INF + || !fileName.equals(ManifestUtil.MANIFEST_FILE) && !ManifestUtil.isSignatureFile(fileName)) { // neither MANIFEST.MF nor one of the sig files + foundNonManifest = true; + break; + } + } + + if (!foundNonManifest) { // only a jar with a manifest and potentially signature, ignore + // don't add to ret + continue; + } + } + } catch (ZipError | Exception e) { + throw new RuntimeException("error reading "+path, e); + } + } + + ret.add(path); + } + + if (!missing.isEmpty()) { + Log.warn(LogCategory.GENERAL, "Class path entries reference missing files: %s - the game may not load properly!", + missing.stream().map(Path::toString).collect(Collectors.joining(", "))); + } + + return ret; + } } diff --git a/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java b/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java index c162461a1..404f01509 100644 --- a/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java +++ b/src/main/java/net/fabricmc/loader/impl/launch/knot/Knot.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -101,31 +100,22 @@ protected ClassLoader init(String[] args) { } } - classPath.clear(); - - List missing = null; - List unsupported = null; + List paths = new ArrayList<>(); + List unsupported = new ArrayList<>(); for (String cpEntry : System.getProperty("java.class.path").split(File.pathSeparator)) { if (cpEntry.equals("*") || cpEntry.endsWith(File.separator + "*")) { - if (unsupported == null) unsupported = new ArrayList<>(); unsupported.add(cpEntry); continue; } - Path path = Paths.get(cpEntry); - - if (!Files.exists(path)) { - if (missing == null) missing = new ArrayList<>(); - missing.add(cpEntry); - continue; - } - - classPath.add(LoaderUtil.normalizeExistingPath(path)); + paths.add(Paths.get(cpEntry)); } - if (unsupported != null) Log.warn(LogCategory.KNOT, "Knot does not support wildcard class path entries: %s - the game may not load properly!", String.join(", ", unsupported)); - if (missing != null) Log.warn(LogCategory.KNOT, "Class path entries reference missing files: %s - the game may not load properly!", String.join(", ", missing)); + if (!unsupported.isEmpty()) Log.warn(LogCategory.KNOT, "Knot does not support wildcard class path entries: %s - the game may not load properly!", String.join(", ", unsupported)); + + classPath.clear(); + classPath.addAll(normalizeClassPath(paths)); provider = createGameProvider(args); Log.finishBuiltinConfig(); diff --git a/src/main/java/net/fabricmc/loader/impl/util/ManifestUtil.java b/src/main/java/net/fabricmc/loader/impl/util/ManifestUtil.java index b04752299..6b3a2f26b 100644 --- a/src/main/java/net/fabricmc/loader/impl/util/ManifestUtil.java +++ b/src/main/java/net/fabricmc/loader/impl/util/ManifestUtil.java @@ -30,9 +30,15 @@ import java.util.List; import java.util.StringTokenizer; import java.util.jar.Attributes.Name; +import java.util.jar.JarFile; import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; public final class ManifestUtil { + public static final String MANIFEST_DIR = "META-INF"; + public static final String MANIFEST_FILE = "MANIFEST.MF"; + public static Manifest readManifest(Class cls) throws IOException, URISyntaxException { CodeSource cs = cls.getProtectionDomain().getCodeSource(); if (cs == null) return null; @@ -62,7 +68,7 @@ public static Manifest readManifest(URL codeSourceUrl) throws IOException, URISy } public static Manifest readManifest(Path basePath) throws IOException { - Path path = basePath.resolve("META-INF").resolve("MANIFEST.MF"); + Path path = basePath.resolve(MANIFEST_DIR).resolve(MANIFEST_FILE); if (!Files.exists(path)) return null; try (InputStream stream = Files.newInputStream(path)) { @@ -70,6 +76,17 @@ public static Manifest readManifest(Path basePath) throws IOException { } } + public static Manifest readManifest(ZipFile zf) throws IOException { + if (zf instanceof JarFile) return ((JarFile) zf).getManifest(); + + ZipEntry entry = zf.getEntry(JarFile.MANIFEST_NAME); + if (entry == null) return null; + + try (InputStream stream = zf.getInputStream(entry)) { + return new Manifest(stream); + } + } + public static String getManifestValue(Manifest manifest, Name name) { return manifest.getMainAttributes().getValue(name); } @@ -88,4 +105,12 @@ public static List getClassPath(Manifest manifest, Path baseDir) throws Mal return ret; } + + public static boolean isSignatureFile(String fileName) { + return fileName.endsWith(".SF") + || fileName.endsWith(".DSA") + || fileName.endsWith(".RSA") + || fileName.endsWith(".EC") + || fileName.startsWith("SIG-"); + } }