package de.geolykt.starloader.gslstarplane; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ThreadLocalRandom; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.UnknownDomainObjectException; import org.gradle.api.artifacts.Configuration; import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.plugins.ide.eclipse.model.Classpath; import org.gradle.plugins.ide.eclipse.model.ClasspathEntry; import org.gradle.plugins.ide.eclipse.model.Container; import org.gradle.plugins.ide.eclipse.model.EclipseModel; import org.jetbrains.annotations.NotNull; import org.jetbrains.java.decompiler.main.Fernflower; import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger.Severity; import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; import org.json.JSONArray; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InnerClassNode; import org.slf4j.LoggerFactory; import de.geolykt.starplane.Autodeobf; import de.geolykt.starplane.JarStripper; import de.geolykt.starplane.JarStripper.MavenId; import de.geolykt.starplane.ObfuscationHandler; import de.geolykt.starplane.remapping.CommentLookup; import de.geolykt.starplane.remapping.MIOContainerFormat; import de.geolykt.starplane.remapping.MIOMappingTreeProvider; import de.geolykt.starplane.sourcegen.EnhancedJarSaver; import de.geolykt.starplane.sourcegen.FernflowerLoggerAdapter; import de.geolykt.starplane.sourcegen.JavadocSource; import net.fabricmc.fernflower.api.IFabricJavadocProvider; public class GslStarplanePlugin implements Plugin { public static final String TASK_GROUP = "GslStarplane"; public static final String GALIM_DEPS_CONFIGURATION_NAME = "galimulatorDependencies"; public static final String DEV_RUNTIME_CONFIGURATION_NAME = "devRuntime"; static final WeakHashMap OBF_HANDLERS = new WeakHashMap<>(); static final WeakHashMap RUN_TASKS = new WeakHashMap<>(); public void apply(Project project) { project.getExtensions().create(GslExtension.class, "starplane", GslExtension.class); project.afterEvaluate(GslStarplanePlugin::runDeobf); project.afterEvaluate(GslStarplanePlugin::setupEEA); project.getTasks().register("remapJar", GslRemapJarTask.class, (task) -> { task.setDescription("Remap deobfuscated jars to use obfuscated mappings."); task.setGroup(GslStarplanePlugin.TASK_GROUP); }); project.getConfigurations().register(DEV_RUNTIME_CONFIGURATION_NAME).configure(configuration -> { configuration.setVisible(false); configuration.setCanBeResolved(true); configuration.setDescription("Dependencies included in the development runtime."); SourceSetContainer sourceSets = (SourceSetContainer) Objects.requireNonNull(project.getProperties().get("sourceSets")); configuration.extendsFrom(project.getConfigurations().getByName(sourceSets.getByName("main").getRuntimeClasspathConfigurationName())); }); project.getTasks().register("deployMods", GslDeployModsTask.class, (task) -> { task.setDescription("Deploy mods to the extension directory of the development environment."); task.setGroup(GslStarplanePlugin.TASK_GROUP); }); GslStarplanePlugin.RUN_TASKS.put(project, project.getTasks().maybeCreate("runMods", GslRunModsTask.class)); project.getTasks().register("genEclipseRuns", GslGenEclipseRunsTask.class, (task) -> { task.setDescription("Generate eclipse *.launch files"); task.setGroup(GslStarplanePlugin.TASK_GROUP); }); } private static void setupEEA(Project project) { GslExtension extension = project.getExtensions().getByType(GslExtension.class); if (extension.eclipseEEA == null) { return; } File eeaPath = project.file(extension.eclipseEEA); EclipseModel eclipseModel = (EclipseModel) project.getProperties().get("eclipse"); if (eclipseModel == null) { LoggerFactory.getLogger(GslStarplanePlugin.class).error("Cannot setup EEA as the eclipse plugin is missing!"); return; } // Based on https://github.com/eclipse/buildship/issues/421#issuecomment-285344240 eclipseModel.getClasspath().containers("org.eclipse.buildship.core.gradleclasspathcontainer"); eclipseModel.getClasspath().file((merger) -> { merger.whenMerged((object) -> { Classpath classpath = (Classpath) object; for (ClasspathEntry entry : classpath.getEntries()) { if (!(entry instanceof Container)) { continue; } Container container = (Container) entry; if (!container.getPath().equals("org.eclipse.buildship.core.gradleclasspathcontainer")) { continue; } container.getEntryAttributes().putIfAbsent("annotationpath", eeaPath.getAbsolutePath().toString()); } }); }); } private static void runDeobf(Project project) { if (GslStarplanePlugin.OBF_HANDLERS.containsKey(project)) { return; } Path altCache = project.getLayout().getBuildDirectory().getAsFile().get().toPath().resolve("gsl-starplane"); GslExtension extension = project.getExtensions().getByType(GslExtension.class); Set<@NotNull Path> softmapFiles = new HashSet<>(); for (Object notation : extension.softmapMappings) { if (notation instanceof Configuration) { for (File f : ((Configuration) notation).resolve()) { softmapFiles.add(f.toPath()); } } else if (notation instanceof Path) { softmapFiles.add((Path) notation); } else { softmapFiles.add(project.file(notation).toPath()); } } List<@NotNull MIOMappingTreeProvider> supplementaryMappings = new ArrayList<>(); for (Map.Entry<@NotNull MIOContainerFormat, @NotNull Object> e : extension.mappings) { Object notation = e.getValue(); if (notation instanceof Configuration) { for (File f : ((Configuration) notation).resolve()) { supplementaryMappings.add(new MIOMappingTreeProvider(e.getKey(), f.toPath())); } } else if (notation instanceof Path) { supplementaryMappings.add(new MIOMappingTreeProvider(e.getKey(), (Path) notation)); } else { supplementaryMappings.add(new MIOMappingTreeProvider(e.getKey(), project.file(notation).toPath())); } } softmapFiles = Collections.unmodifiableSet(softmapFiles); supplementaryMappings = Collections.unmodifiableList(supplementaryMappings); ObfuscationHandler oHandler = new ObfuscationHandler(altCache, project.getProjectDir().toPath(), extension.getRASContents(project), softmapFiles, supplementaryMappings); GslStarplanePlugin.OBF_HANDLERS.put(project, oHandler); resolve(project, oHandler); JavaExec runTask = GslStarplanePlugin.RUN_TASKS.get(project); if (runTask != null) { runTask.systemProperty("de.geolykt.starloader.launcher.CLILauncher.mainClass", "com.example.Main"); Path modsDir = extension.modDirectory; if (modsDir == null) { modsDir = runTask.getWorkingDir().toPath().resolve("mods"); } runTask.systemProperty("de.geolykt.starloader.launcher.IDELauncher.modDirectory", modsDir.toAbsolutePath().toString()); } } static String getBootPath(Project p) { JSONArray bootPath = new JSONArray(); try { bootPath.put(GslStarplanePlugin.OBF_HANDLERS.get(p).getTransformedGalimulatorJar().toAbsolutePath().resolveSibling("galimulator-remapped-rt.jar").toUri().toURL().toExternalForm()); for (File f : p.getConfigurations().getByName(GslStarplanePlugin.GALIM_DEPS_CONFIGURATION_NAME).resolve()) { bootPath.put(f.toURI().toURL().toExternalForm()); } } catch (MalformedURLException e) { e.printStackTrace(); } return "-Dde.geolykt.starloader.launcher.IDELauncher.bootURLs=" + bootPath.toString(); } public static void resolve(Project project, ObfuscationHandler obfHandler) { // compileLarge = complete galimulator jar with compile access (intermediary product) // compileStripped = stripped galimulator jar with compile access // compileStrippedSource = decompiled stripped galimulator jar with compile-time accesss // runtimeLarge = complete galimulator jar with runtime access Path compileLarge = obfHandler.getTransformedGalimulatorJar(); Path compileStripped = compileLarge.resolveSibling("galimulator-remapped-stripped-" + Autodeobf.getVersion() + ".jar"); Path compileStrippedSource = compileLarge.resolveSibling("galimulator-remapped-stripped-" + Autodeobf.getVersion() + "-sources.jar"); Path runtimeLarge = compileLarge.resolveSibling("galimulator-remapped-rt.jar"); JarStripper stripper = new JarStripper(); NamedDomainObjectProvider galimDepsConfig = null; try { galimDepsConfig = project.getConfigurations().named(GslStarplanePlugin.GALIM_DEPS_CONFIGURATION_NAME); } catch (UnknownDomainObjectException e) { galimDepsConfig = project.getConfigurations().register(GslStarplanePlugin.GALIM_DEPS_CONFIGURATION_NAME); } Set deps; // We could probably make that step quicker (through caching) but whatever - this works (TM) try (InputStream is = Files.newInputStream(compileLarge)) { deps = stripper.getShadedDependencies(is); } catch (IOException e) { throw new UncheckedIOException(e); } for (MavenId dep : deps) { project.getDependencies().add(GslStarplanePlugin.GALIM_DEPS_CONFIGURATION_NAME, dep.toGAVNotation()); } Set transitiveDeps = galimDepsConfig.get().resolve(); if (obfHandler.didRefresh || Files.notExists(compileStripped)) { try { Set removePaths = new HashSet<>(); removePaths.remove("META-INF/MANIFEST.MF"); for (File transitiveDep : transitiveDeps) { try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(transitiveDep.toPath()))) { for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) { String name = entry.getName(); if (name.codePointAt(0) == '/') { name = name.substring(0); } removePaths.add(name); } } } stripper.createStrippedJar(compileLarge, compileStripped, removePaths); } catch (IOException e) { throw new UncheckedIOException(e); } } if (obfHandler.didRefresh || Files.notExists(compileStrippedSource) && !Boolean.getBoolean("org.stianloader.starplane.skipDecompile")) { try { GslStarplanePlugin.decompile(project, runtimeLarge, compileStripped, compileStrippedSource, transitiveDeps, obfHandler.getJavadocLookup()); } catch (IOException e) { throw new UncheckedIOException(e); } } project.getRepositories().flatDir((repo) -> { repo.dir(compileLarge.getParent()); repo.setName("generated-galimulator-remapped"); }); project.getDependencies().add("compileOnly", ":galimulator-remapped-stripped:" + Autodeobf.getVersion()); obfHandler.didRefresh = false; // Everything else was reset so we can dare to reset that flag should this method be called multiple times } private static void decompile(Project project, @NotNull Path runtimeLarge, @NotNull Path compileStripped, @NotNull Path compileStrippedSource, Set transitiveDeps, @NotNull CommentLookup javadocLookup) throws IOException { project.getLogger().info("Decompiling galimulator"); // Time to decompile that stripped jar Map args = new HashMap<>(); args.put(IFernflowerPreferences.INDENT_STRING, " "); // Default is 3 Spaces, which is nonsense args.put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1"); // Default is false, which is nonsense args.put(IFernflowerPreferences.INCLUDE_ENTIRE_CLASSPATH, "1"); args.put(IFernflowerPreferences.LOG_LEVEL, "WARN"); args.put(IFernflowerPreferences.VERIFY_ANONYMOUS_CLASSES, "1"); args.put(IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1"); args.put(IFernflowerPreferences.DUMP_CODE_LINES, "1"); args.put(IFernflowerPreferences.DUMP_ORIGINAL_LINES, "1"); args.put(IFernflowerPreferences.REMOVE_SYNTHETIC, "0"); // While it is a nice tool to see how good our deobfuscator is, sometimes it isn't that good args.put(IFabricJavadocProvider.PROPERTY_NAME, new JavadocSource(javadocLookup)); Map lineMappings = new HashMap<>(); try (EnhancedJarSaver jarSaver = new EnhancedJarSaver(compileStrippedSource.toFile(), lineMappings)) { Fernflower qf = new Fernflower(jarSaver, args, new FernflowerLoggerAdapter(Severity.WARN)); qf.addSource(compileStripped.toFile()); for (File transitiveDep : transitiveDeps) { qf.addLibrary(transitiveDep); } qf.decompileContext(); } project.getLogger().info("Galimulator decompiled"); project.getLogger().info("Replacing line mappings"); replaceLineNumbers(compileStripped, lineMappings); replaceLineNumbers(runtimeLarge, lineMappings); project.getLogger().info("Line mappings replaced"); } private static void replaceLineNumbers(@NotNull Path lineReplaceTarget, Map lineMappings) throws IOException { Map nameToNode = new LinkedHashMap<>(); try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(lineReplaceTarget), StandardCharsets.UTF_8)) { for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) { if (entry.getName().endsWith(".class")) { ClassNode node = new ClassNode(); ClassReader reader = new ClassReader(zipIn); reader.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); nameToNode.put(node.name, node); } } } Path intermediary = Files.createTempFile("gslstarplane-linereplace-" + ThreadLocalRandom.current().nextInt(), ".jar"); try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(lineReplaceTarget), StandardCharsets.UTF_8); ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(intermediary), StandardCharsets.UTF_8)) { for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) { zipOutputStream.putNextEntry(new ZipEntry(entry.getName())); if (!entry.getName().endsWith(".class")) { byte[] buffer = new byte[4096]; for (int read = zipIn.read(buffer); read != -1; read = zipIn.read(buffer)) { zipOutputStream.write(buffer, 0, read); } continue; } ClassReader reader = new ClassReader(zipIn); ClassNode node = nameToNode.get(reader.getClassName()); ClassNode outermostClassnode = node; outermostNodeFinderLoop: while (true) { if (outermostClassnode.outerClass != null) { outermostClassnode = nameToNode.get(outermostClassnode.outerClass); continue; } for (InnerClassNode icn : outermostClassnode.innerClasses) { if (icn.name.equals(outermostClassnode.name) && icn.outerName != null) { outermostClassnode = nameToNode.get(icn.outerName); continue outermostNodeFinderLoop; } } break; } if (node.sourceFile == null || node.sourceFile.equals("SourceFile")) { int startName = outermostClassnode.name.lastIndexOf('/') + 1; int innerSeperator = outermostClassnode.name.indexOf('$'); String baseName; if (innerSeperator == -1) { baseName = outermostClassnode.name.substring(startName); } else { baseName = outermostClassnode.name.substring(startName, innerSeperator); } node.sourceFile = baseName + ".java"; } // mapping[i * 2] -> original line number; mapping[i * 2 + 1] -> new line number int[] mapping = lineMappings.get(outermostClassnode.name); Map lineNumberConversion; if (mapping == null) { lineNumberConversion = null; } else { lineNumberConversion = new HashMap<>(); for (int i = 0; i < mapping.length;) { lineNumberConversion.put(mapping[i++], mapping[i++]); } } ClassWriter writer = new ClassWriter(reader, 0); reader.accept(new ClassVisitor(Opcodes.ASM9, writer) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { return new MethodVisitor(this.api, super.visitMethod(access, name, descriptor, signature, exceptions)) { @Override public void visitLineNumber(int line, Label start) { if (lineNumberConversion == null) { super.visitLineNumber(line, start); } else { Integer newLineNumber = lineNumberConversion.get(line); if (newLineNumber == null) { super.visitLineNumber(line, start); } else { super.visitLineNumber(newLineNumber.intValue(), start); } } } }; } @Override public void visitSource(String source, String debug) { super.visitSource(node.sourceFile, debug); } }, 0); zipOutputStream.write(writer.toByteArray()); } } Files.move(intermediary, lineReplaceTarget, StandardCopyOption.REPLACE_EXISTING); nameToNode.clear(); } }