diff --git a/build.gradle b/build.gradle index 1672472..51f8285 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ targetCompatibility = 8 group = "com.unascribed" archivesBaseName = "NilLoader" -version = "1.1.4" +version = "1.1.5" tasks.withType(JavaCompile) { options.release = 8 @@ -17,6 +17,12 @@ tasks.withType(JavaCompile) { repositories { mavenCentral() + maven { + url 'https://maven.quiltmc.org/repository/release/' + content { + includeGroup 'org.quiltmc' + } + } } configurations { @@ -35,6 +41,7 @@ dependencies { shade 'com.grack:nanojson:1.7' + compileOnly 'org.quiltmc:quiltflower:1.8.1' compileOnly 'org.slf4j:slf4j-api:1.7.9' compileOnly 'org.apache.logging.log4j:log4j-api:2.17.1' compileOnly 'log4j:log4j:1.2.17' diff --git a/src/main/java/nilloader/Decompiler.java b/src/main/java/nilloader/Decompiler.java new file mode 100644 index 0000000..8b6b01a --- /dev/null +++ b/src/main/java/nilloader/Decompiler.java @@ -0,0 +1,169 @@ +package nilloader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.jetbrains.java.decompiler.main.Fernflower; +import org.jetbrains.java.decompiler.main.extern.IBytecodeProvider; +import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; +import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; + +import nilloader.api.NilLogger; + +class Decompiler { + + private interface QuiltflowerAccess { + String decompile(String name, byte[] clazz); + } + + private static final class QuiltflowerAccessImpl implements QuiltflowerAccess { + + private final NilLogger log = NilLogger.get("Quiltflower"); + + @Override + public String decompile(String name, byte[] clazz) { + Map options = new HashMap<>(); + options.put(IFernflowerPreferences.INCLUDE_ENTIRE_CLASSPATH, true); + options.put(IFernflowerPreferences.INCLUDE_JAVA_RUNTIME, true); + options.put(IFernflowerPreferences.THREADS, 4); + options.put(IFernflowerPreferences.INDENT_STRING, "\t"); + options.put(IFernflowerPreferences.FINALLY_DEINLINE, true); + options.put(IFernflowerPreferences.USE_METHOD_PARAMETERS, true); + options.put(IFernflowerPreferences.USE_DEBUG_VAR_NAMES, true); + options.put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, true); + String[] contentArr = new String[] { "// decompilation failed" }; + Fernflower ff = new Fernflower(new IBytecodeProvider() { + + @Override + public byte[] getBytecode(String externalPath, String internalPath) throws IOException { + if (externalPath != null && externalPath.endsWith(".nil/bad-fernflower-workaround/this-file-does-not-exist.class")) { + return clazz; + } + return new byte[0]; + } + }, new IResultSaver() { + @Override public void saveFolder(String path) {} + @Override public void saveDirEntry(String path, String archiveName, String entryName) {} + @Override public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { + contentArr[0] = content; + } + @Override public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) { + contentArr[0] = content; + } + @Override public void createArchive(String path, String archiveName, Manifest manifest) {} + @Override public void copyFile(String source, String path, String entryName) {} + @Override public void copyEntry(String source, String path, String archiveName, String entry) {} + @Override public void closeArchive(String path, String archiveName) {} + }, options, new IFernflowerLogger() { + + @Override + public void writeMessage(String message, Severity severity, Throwable t) { + if (severity == Severity.WARN) { + log.warn(message, t); + } else if (severity == Severity.ERROR) { + log.error(message, t); + } + } + + @Override + public void writeMessage(String message, Severity severity) { + if (severity == Severity.WARN) { + log.warn(message); + } else if (severity == Severity.ERROR) { + log.error(message); + } + } + + }); + try { + ff.addSource(new File(".nil/bad-fernflower-workaround/this-file-does-not-exist.class")); + try { + ff.decompileContext(); + } finally { + ff.clearContext(); + } + return contentArr[0]; + } catch (Throwable e) { + log.error("Failed to decompile", e); + return "// decompilation failed"; + } + } + + } + + private static boolean failure = false; + private static QuiltflowerAccess access = null; + + static void initialize() { + try { + File dotNil = new File(".nil"); + File quiltflower = new File(dotNil, "quiltflower.jar"); + if (!quiltflower.exists()) { + NilLoaderLog.log.info("Downloading Quiltflower..."); + try { + URL u = new URL("https://maven.quiltmc.org/repository/release/org/quiltmc/quiltflower/1.8.1/quiltflower-1.8.1.jar"); + String expectedHashB64 = "79hnFEabe9+TEz1KefC7gcugcmuCcz3N1Ok/80kolcs="; + byte[] expectedHash = Base64.getDecoder().decode(expectedHashB64); + int expectedSize = 874457; + dotNil.mkdirs(); + File tmp = File.createTempFile("quiltflower", ".jar.part", dotNil); + byte[] buf = new byte[4096]; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + int realSize = 0; + try (InputStream in = u.openStream(); OutputStream out = new FileOutputStream(tmp)) { + while (true) { + int read = in.read(buf); + if (read == -1) break; + realSize += read; + if (realSize > expectedSize) throw new IOException("File is longer than expected length "+expectedSize); + out.write(buf, 0, read); + digest.update(buf, 0, read); + } + } + byte[] realHash = digest.digest(); + if (!Arrays.equals(expectedHash, realHash)) { + throw new IOException("File hash is incorrect (got "+Base64.getEncoder().encodeToString(realHash)+", but expected "+expectedHashB64+")"); + } + tmp.renameTo(quiltflower); + } catch (IOException | NoSuchAlgorithmException e) { + NilLoaderLog.log.error("Failed to download Quiltflower", e); + failure = true; + return; + } + } + NilLoader.injectToSearchPath(new JarFile(quiltflower)); + access = new QuiltflowerAccessImpl(); + } catch (Throwable t) { + NilLoaderLog.log.error("Failed to load Quiltflower", t); + failure = true; + return; + } + } + + static void perform(String name, byte[] before, byte[] after) { + try { + if (failure) return; + String beforeCode = access.decompile(name, before); + String afterCode = access.decompile(name, after); + NilLoader.writeDump(name, beforeCode.getBytes(StandardCharsets.UTF_8), "before", "java"); + NilLoader.writeDump(name, afterCode.getBytes(StandardCharsets.UTF_8), "after", "java"); + } catch (Throwable t) { + NilLoaderLog.log.error("Failed to perform decompile diff for {}", name, t); + } + } + +} diff --git a/src/main/java/nilloader/NilLoader.java b/src/main/java/nilloader/NilLoader.java index c48d7cf..8767196 100644 --- a/src/main/java/nilloader/NilLoader.java +++ b/src/main/java/nilloader/NilLoader.java @@ -1,9 +1,11 @@ package nilloader; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.PrintStream; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; @@ -11,6 +13,7 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; @@ -31,6 +34,7 @@ import org.cadixdev.bombe.type.signature.MethodSignature; import org.cadixdev.lorenz.MappingSet; import org.cadixdev.lorenz.asm.LorenzRemapper; +import org.cadixdev.lorenz.io.srg.tsrg.TSrgReader; import org.cadixdev.lorenz.model.ClassMapping; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; @@ -40,6 +44,7 @@ import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; + import nilloader.api.ClassTransformer; import nilloader.api.NilMetadata; import nilloader.api.lib.mini.MiniTransformer; @@ -48,8 +53,13 @@ public class NilLoader { - private static final boolean DUMP = Boolean.getBoolean("nil.debug.dump"); + private static final boolean DEBUG_DUMP = Boolean.getBoolean("nil.debug.dump"); + private static final boolean DEBUG_DECOMPILE = Boolean.getBoolean("nil.debug.decompile"); + private static final boolean DEBUG_FLIP_DIR_LAYOUT = Boolean.getBoolean("nil.debug.dump.flipDirLayout") || Boolean.getBoolean("nil.debug.decompile.flipDirLayout"); private static final boolean DEBUG_CLASSLOADING = Boolean.getBoolean("nil.debug.classLoading"); + private static final String DEBUG_MAPPINGS_PATH = System.getProperty("nil.debug.mappings"); + + private static MappingSet debugMappings = null; private static final class EntrypointListener { public final String id; @@ -78,6 +88,8 @@ public EntrypointListener(String id, String className) { private static Set loadedClasses = new HashSet<>(); + private static Instrumentation instrumentation; + private static String activeMod = null; private static int initializations = 0; private static int nilAgents = 1; @@ -96,6 +108,7 @@ public static void premain(String arg, Instrumentation ins) { } return; } + instrumentation = ins; if (DEBUG_CLASSLOADING) { PrintStream err = System.err; ins.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> { @@ -112,12 +125,22 @@ public static void premain(String arg, Instrumentation ins) { builtInTransformers.add(new RelaunchClassLoaderTransformer()); ins.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> { if (classBeingRedefined != null || className == null) return classfileBuffer; - return NilLoader.transform(builtInTransformers, className, classfileBuffer); + return NilLoader.transform(loader, builtInTransformers, className, classfileBuffer); }); for (Runnable r : NilLogManager.initLogs) { r.run(); } NilLogManager.initLogs.clear(); + if (DEBUG_DECOMPILE) { + Decompiler.initialize(); + } + if (DEBUG_MAPPINGS_PATH != null) { + try (InputStreamReader r = new InputStreamReader(new FileInputStream(new File(DEBUG_MAPPINGS_PATH)), StandardCharsets.UTF_8)) { + debugMappings = new TSrgReader(r).read(); + } catch (IOException e) { + NilLoaderLog.log.error("Failed to load debug mappings", e); + } + } try { discover("nilloader", NilLoader.class.getClassLoader(), new File(NilLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI())); } catch (URISyntaxException e) { @@ -226,22 +249,17 @@ private static void completePremain(Instrumentation ins) { MappingSet mappings = getActiveMappings(definer); if (mappings != null) { NilLoaderLog.log.debug("Remapping mod class {} via mapping set {}", className, NilLoader.getActiveMappingId(definer)); - ClassProviderInheritanceProvider cpip = new ClassProviderInheritanceProvider(Opcodes.ASM9, new ClassLoaderClassProvider(loader)); - LorenzRemapper lr = new LorenzRemapper(mappings, cpip); - ClassReader reader = new ClassReader(classfileBuffer); - ClassWriter writer = new ClassWriter(reader, 0); - ClassRemapper cr = new ClassRemapper(writer, lr); - reader.accept(cr, 0); - classfileBuffer = writer.toByteArray(); + classfileBuffer = remap(loader, classfileBuffer, mappings); } } } - return NilLoader.transform(transformers, className, classfileBuffer); + return NilLoader.transform(loader, transformers, className, classfileBuffer); }); fireEntrypoint("premain"); NilLoaderLog.log.debug("{} class transformer{} registered", transformers.size(), transformers.size() == 1 ? "" : "s"); frozen = true; // clean up stuff we won't be using anymore + instrumentation = null; ins.removeTransformer(loadTracker); loadedClasses = null; for (Map.Entry> en : modMappings.entrySet()) { @@ -250,6 +268,16 @@ private static void completePremain(Instrumentation ins) { } } + private static byte[] remap(ClassLoader loader, byte[] clazz, MappingSet mappings) { + ClassProviderInheritanceProvider cpip = new ClassProviderInheritanceProvider(Opcodes.ASM9, new ClassLoaderClassProvider(loader)); + LorenzRemapper lr = new LorenzRemapper(mappings, cpip); + ClassReader reader = new ClassReader(clazz); + ClassWriter writer = new ClassWriter(reader, 0); + ClassRemapper cr = new ClassRemapper(writer, lr); + reader.accept(cr, 0); + return writer.toByteArray(); + } + private static void discoverDirectory(File dir, String... extensions) { NilLoaderLog.log.debug("Searching for nilmods in ./{}", dir.getName()); String[] trailers = new String[extensions.length]; @@ -422,9 +450,9 @@ private static void install(NilMetadata meta) { NilLoaderLog.log.debug("Installing discovered mod {} (ID {}) v{} from {}", meta.name, meta.id, meta.version, meta.source); mods.put(meta.id, meta); } - - public static byte[] transform(List transformers, String className, byte[] classBytes) { - byte[] orig = DUMP ? classBytes : null; + + public static byte[] transform(ClassLoader loader, List transformers, String className, byte[] classBytes) { + byte[] orig = DEBUG_DUMP || DEBUG_DECOMPILE ? classBytes : null; try { boolean changed = false; for (ClassTransformer ct : transformers) { @@ -436,24 +464,42 @@ public static byte[] transform(List transformers, String class NilLoaderLog.log.error("Failed to transform {} via {}", className, ct.getClass().getName(), t); } } - if (changed && DUMP) { - writeDump(className, orig, "before"); - writeDump(className, classBytes, "after"); + if (changed) { + String dumpName = className; + byte[] before = orig; + byte[] after = classBytes; + if (debugMappings != null) { + try { + before = remap(loader, before, debugMappings); + after = remap(loader, after, debugMappings); + dumpName = debugMappings.computeClassMapping(dumpName).map(ClassMapping::getFullDeobfuscatedName).orElse(className).replace('/', '.'); + } catch (Throwable t) { + NilLoaderLog.log.error("Failed to remap {}", className, t); + } + } + if (DEBUG_DUMP) { + writeDump(dumpName, before, "before", "class"); + writeDump(dumpName, after, "after", "class"); + } + if (DEBUG_DECOMPILE) { + Decompiler.perform(dumpName, before, after); + } } return classBytes; } catch (RuntimeException t) { - if (DUMP) writeDump(className, orig, "before"); + if (DEBUG_DUMP) writeDump(className, orig, "before", "class"); throw t; } catch (Error e) { - if (DUMP) writeDump(className, orig, "before"); + if (DEBUG_DUMP) writeDump(className, orig, "before", "class"); throw e; } } - private static void writeDump(String className, byte[] classBytes, String what) { - File dir = new File(".nil/debug-out", className.replace('/', '.')); + static void writeDump(String className, byte[] classBytes, String what, String ext) { + String classNameDots = className.replace('/', '.'); + File dir = new File(".nil/debug-out", DEBUG_FLIP_DIR_LAYOUT ? what : classNameDots); dir.mkdirs(); - File f = new File(dir, what+".class"); + File f = new File(dir, (DEBUG_FLIP_DIR_LAYOUT ? classNameDots : what)+"."+ext); try { FileOutputStream fos = new FileOutputStream(f); try { @@ -462,7 +508,7 @@ private static void writeDump(String className, byte[] classBytes, String what) fos.close(); } } catch (IOException e) { - NilLoaderLog.log.debug("Failed to write before class to {}", f, e); + NilLoaderLog.log.debug("Failed to write {} {} to {}", what, ext, f, e); } } @@ -530,4 +576,8 @@ public static boolean isModLoaded(String id) { return mods.containsKey(id); } + static void injectToSearchPath(JarFile file) { + instrumentation.appendToSystemClassLoaderSearch(file); + } + }