From 22002c1507a4dc576dc0eb907d366a56d6beb4fd Mon Sep 17 00:00:00 2001 From: LexManos Date: Thu, 1 Feb 2024 16:55:32 -0800 Subject: [PATCH] Add support for white and blacklist via command line Add support for bundled server jars --- Jenkinsfile | 93 ----- LICENSE-header.txt | 18 +- build.gradle | 36 +- .../mergetool/AnnotationVersion.java | 18 +- .../mergetool/ConsoleMerger.java | 178 +++++--- .../net/minecraftforge/mergetool/Merger.java | 390 +++++++++--------- .../minecraftforge/mergetool/Stripper.java | 74 ++-- 7 files changed, 356 insertions(+), 451 deletions(-) delete mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 33765da..0000000 --- a/Jenkinsfile +++ /dev/null @@ -1,93 +0,0 @@ -@Library('forge-shared-library')_ - -pipeline { - agent { - docker { - image 'gradle:jdk8' - args '-v gradlecache:/gradlecache' - } - } - environment { - GRADLE_ARGS = '-Dorg.gradle.daemon.idletimeout=5000' - DISCORD_WEBHOOK = credentials('forge-discord-jenkins-webhook') - DISCORD_PREFIX = "Job: MergeTool Branch: ${BRANCH_NAME} Build: #${BUILD_NUMBER}" - JENKINS_HEAD = 'https://wiki.jenkins-ci.org/download/attachments/2916393/headshot.png' - } - - stages { - stage('fetch') { - steps { - checkout scm - } - } - stage('notify_start') { - when { - not { - changeRequest() - } - } - steps { - discordSend( - title: "${DISCORD_PREFIX} Started", - successful: true, - result: 'ABORTED', //White border - thumbnail: JENKINS_HEAD, - webhookURL: DISCORD_WEBHOOK - ) - } - } - stage('buildandtest') { - steps { - withGradle { - sh './gradlew ${GRADLE_ARGS} --refresh-dependencies --continue build test' - } - script { - env.MYGROUP = sh(returnStdout: true, script: './gradlew properties -q | grep "group:" | awk \'{print $2}\'').trim() - env.MYARTIFACT = sh(returnStdout: true, script: './gradlew properties -q | grep "name:" | awk \'{print $2}\'').trim() - env.MYVERSION = sh(returnStdout: true, script: './gradlew properties -q | grep "version:" | awk \'{print $2}\'').trim() - } - } - post { - success { - writeChangelog(currentBuild, 'build/changelog.txt') - archiveArtifacts artifacts: 'build/changelog.txt', fingerprint: false - } - } - } - stage('publish') { - when { - not { - changeRequest() - } - } - steps { - withCredentials([usernamePassword(credentialsId: 'maven-forge-user', usernameVariable: 'MAVEN_USER', passwordVariable: 'MAVEN_PASSWORD')]) { - withGradle { - sh './gradlew ${GRADLE_ARGS} publish' - } - } - } - post { - success { - build job: 'filegenerator', parameters: [string(name: 'COMMAND', value: "promote ${env.MYGROUP}:${env.MYARTIFACT} ${env.MYVERSION} latest")], propagate: false, wait: false - } - } - } - } - post { - always { - script { - if (env.CHANGE_ID == null) { // This is unset for non-PRs - discordSend( - title: "${DISCORD_PREFIX} Finished ${currentBuild.currentResult}", - description: '```\n' + getChanges(currentBuild) + '\n```', - successful: currentBuild.resultIsBetterOrEqualTo("SUCCESS"), - result: currentBuild.currentResult, - thumbnail: JENKINS_HEAD, - webhookURL: DISCORD_WEBHOOK - ) - } - } - } - } -} diff --git a/LICENSE-header.txt b/LICENSE-header.txt index 49e1dcf..a43a5e3 100644 --- a/LICENSE-header.txt +++ b/LICENSE-header.txt @@ -1,16 +1,2 @@ -MergeTool -Copyright (c) 2016-2018. - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation version 2.1 -of the License. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA \ No newline at end of file +Copyright (c) Forge Development LLC +SPDX-License-Identifier: LGPL-2.1-only \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7e78288..4ac9b46 100644 --- a/build.gradle +++ b/build.gradle @@ -5,10 +5,10 @@ plugins { id 'java' id 'eclipse' id 'maven-publish' - id 'org.cadixdev.licenser' version '0.6.1' + id 'net.minecraftforge.licenser' version '1.0.1' id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'com.github.ben-manes.versions' version '0.47.0' - id 'net.minecraftforge.gradleutils' version '2.+' + id 'com.github.ben-manes.versions' version '0.51.0' + id 'net.minecraftforge.gradleutils' version '[2.3,2.4]' } group = 'net.minecraftforge' @@ -32,12 +32,17 @@ java { withSourcesJar() } -tasks.named('jar', Jar).configure { +tasks.register('copyMarkers', Copy).configure { from(configurations.markers.collect { zipTree(it) } ) { into 'markers' include '**/*.class' rename { it.replace('.class', '.marker') } } + includeEmptyDirs = false + into project.layout.buildDirectory.dir('copyMarkers') +} + +tasks.named('jar', Jar).configure { manifest { attributes('Main-Class': 'net.minecraftforge.mergetool.ConsoleMerger') attributes('Implementation-Version': project.version) @@ -48,9 +53,16 @@ tasks.named('shadowJar', ShadowJar).configure { archiveClassifier = 'fatjar' } +sourceSets { + main { + resources { + srcDirs += [tasks.named('copyMarkers')] + } + } +} + configurations { markers - implementation.extendsFrom(markers) } dependencies { @@ -58,17 +70,13 @@ dependencies { implementation 'org.ow2.asm:asm-tree:9.5' implementation 'org.ow2.asm:asm-util:9.5' implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' - implementation 'net.minecraftforge:srgutils:0.4.15' - + implementation 'net.minecraftforge:srgutils:0.5.7' + markers 'net.minecraftforge:mergetool-cpw:1.0' markers 'net.minecraftforge:mergetool-fml:1.0' markers 'net.minecraftforge:mergetool-api:1.0' } -artifacts { - archives shadowJar -} - publishing { publications.register('mavenJava', MavenPublication) { from components.java @@ -79,7 +87,7 @@ publishing { name = 'MergeTool' description = 'Merges two jar files together, useful for rebuilding Retroguard stripped jars.' url = 'https://github.com/MinecraftForge/MergeTool' - + PomUtils.setGitHubDetails(pom, 'MergeTool') license PomUtils.Licenses.LGPLv2_1 @@ -89,8 +97,8 @@ publishing { } } } - + repositories { - maven gradleutils.getPublishingForgeMaven() + maven gradleutils.publishingForgeMaven } } diff --git a/src/main/java/net/minecraftforge/mergetool/AnnotationVersion.java b/src/main/java/net/minecraftforge/mergetool/AnnotationVersion.java index 53a050a..9b04bc6 100644 --- a/src/main/java/net/minecraftforge/mergetool/AnnotationVersion.java +++ b/src/main/java/net/minecraftforge/mergetool/AnnotationVersion.java @@ -1,20 +1,6 @@ /* - * MergeTool - * Copyright (c) 2016-2018. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only */ package net.minecraftforge.mergetool; diff --git a/src/main/java/net/minecraftforge/mergetool/ConsoleMerger.java b/src/main/java/net/minecraftforge/mergetool/ConsoleMerger.java index 7549d3e..945d3b8 100644 --- a/src/main/java/net/minecraftforge/mergetool/ConsoleMerger.java +++ b/src/main/java/net/minecraftforge/mergetool/ConsoleMerger.java @@ -1,80 +1,59 @@ /* - * MergeTool - * Copyright (c) 2016-2018. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only */ package net.minecraftforge.mergetool; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; + import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; import joptsimple.ValueConverter; +import net.minecraftforge.srgutils.IMappingFile; +import net.minecraftforge.srgutils.IMappingFile.IClass; -public class ConsoleMerger -{ +public class ConsoleMerger { private static enum Tasks { MERGE, STRIP }; - private static final ValueConverter AnnotationReader = new ValueConverter() - { + private static final ValueConverter AnnotationReader = new ValueConverter() { @Override - public AnnotationVersion convert(String value) - { - try - { + public AnnotationVersion convert(String value) { + try { return AnnotationVersion.valueOf(value.toUpperCase(Locale.ENGLISH)); - } - catch (IllegalArgumentException e) //Invalid argument, lets try by version, wish there was a way to know before hand. - { + } catch (IllegalArgumentException e) { //Invalid argument, lets try by version, wish there was a way to know before hand. return AnnotationVersion.fromVersion(value); } } @Override - public Class valueType() - { + public Class valueType() { return AnnotationVersion.class; } @Override - public String valuePattern() - { + public String valuePattern() { return null; } }; - public static void main(String[] args) - { + public static void main(String[] args) { List extra = new ArrayList<>(); Tasks task = null; - for (int x = 0; x < args.length; x++) - { - if ("--strip".equals(args[x])) - { + for (int x = 0; x < args.length; x++) { + if ("--strip".equals(args[x])) { if (task != null) throw new IllegalArgumentException("Only one task supported at a time: " + task); task = Tasks.STRIP; - } - else if ("--merge".equals(args[x])) - { + } else if ("--merge".equals(args[x])) { if (task != null) throw new IllegalArgumentException("Only one task supported at a time: " + task); task = Tasks.MERGE; @@ -89,8 +68,7 @@ else if (task == Tasks.STRIP) strip(extra.toArray(new String[extra.size()])); } - private static void merge(String[] args) - { + private static void merge(String[] args) { OptionParser parser = new OptionParser(); OptionSpec client = parser.accepts("client").withRequiredArg().ofType(File.class).required(); OptionSpec server = parser.accepts("server").withRequiredArg().ofType(File.class).required(); @@ -99,9 +77,15 @@ private static void merge(String[] args) OptionSpec data = parser.accepts("keep-data"); OptionSpec meta = parser.accepts("keep-meta"); OptionSpec anno = parser.accepts("ann").withOptionalArg().ofType(AnnotationVersion.class).withValuesConvertedBy(AnnotationReader).defaultsTo(AnnotationVersion.API); - - try - { + OptionSpec whitelist = parser.accepts("whitelist").withRequiredArg().ofType(String.class); + OptionSpec whitelistPkg = parser.accepts("whitelist-pkg").withRequiredArg().ofType(String.class); + OptionSpec whitelistMap = parser.accepts("whitelist-map").withRequiredArg().ofType(File.class); + OptionSpec blacklist = parser.accepts("blacklist").withRequiredArg().ofType(String.class); + OptionSpec blacklistPkg = parser.accepts("blacklist-pkg").withRequiredArg().ofType(String.class); + OptionSpec blacklistMap = parser.accepts("blacklist-map").withRequiredArg().ofType(File.class); + OptionSpec bundled = parser.accepts("bundled"); + + try { OptionSet options = parser.parse(args); File client_jar = options.valueOf(client); @@ -122,44 +106,77 @@ private static void merge(String[] args) if (options.has(meta)) merge.keepMeta(); - try - { - merge.process(); + if (options.has(bundled)) + merge.bundledServerJar(); + + Predicate filter = null; + if (options.has(whitelist) || options.has(whitelistMap)) { + Set classes = loadList(options.valuesOf(whitelist), options.valuesOf(whitelistMap)); + if (!classes.isEmpty()) + filter = classes::contains; } - catch (IOException e) - { - e.printStackTrace(); + + if (options.has(whitelistPkg)) { + Set pkgs = loadPackages(options.valuesOf(whitelistPkg)); + Predicate prefixes = name -> { + int idx = name.lastIndexOf('/'); + return idx == -1 ? pkgs.contains("/") : pkgs.contains(name.substring(0, idx)); + }; + + if (filter == null) + filter = prefixes; + else + filter = filter.or(prefixes); } - } - catch (OptionException e) - { + + if (filter == null) + filter = name -> true; + + if (options.has(blacklist) || options.has(blacklistMap)) { + Set classes = loadList(options.valuesOf(blacklist), options.valuesOf(blacklistMap)); + if (!classes.isEmpty()) + filter.and(name -> !classes.contains(name)); + } + + if (options.has(blacklistPkg)) { + Set pkgs = loadPackages(options.valuesOf(blacklistPkg)); + filter.and(name -> { + int idx = name.lastIndexOf('/'); + return idx == -1 ? !pkgs.contains("/") : !pkgs.contains(name.substring(0, idx)); + }); + } + + merge.filter(filter); + + merge.process(); + } catch (OptionException e) { System.out.println("Usage: ConsoleMerger --merge --client --server --output [--ann CPW|NMF|API] [--keep-data] [--keep-meta]"); e.printStackTrace(); + sneak(e); + } catch (IOException e) { + e.printStackTrace(); + sneak(e); } } - private static void strip(String[] args) - { + private static void strip(String[] args) { OptionParser parser = new OptionParser(); OptionSpec input = parser.accepts("input").withRequiredArg().ofType(File.class).required(); OptionSpec output = parser.accepts("output").withRequiredArg().ofType(File.class).required(); OptionSpec data = parser.accepts("data").withRequiredArg().ofType(File.class).required(); - try - { + try { OptionSet options = parser.parse(args); File input_jar = options.valueOf(input).getAbsoluteFile(); File output_jar = options.valueOf(output).getAbsoluteFile(); - try - { + try { System.out.println("Input: " + input_jar); System.out.println("Output: " + output_jar); Stripper strip = new Stripper(); - for (File dataF : options.valuesOf(data)) - { + for (File dataF : options.valuesOf(data)) { System.out.println("Data: " + dataF.getAbsoluteFile()); strip.loadData(dataF); } @@ -168,16 +185,41 @@ private static void strip(String[] args) System.out.println("Could not delete output file: " + output_jar); strip.process(input_jar, output_jar); - } - catch (IOException e) - { + } catch (IOException e) { e.printStackTrace(); } - } - catch (OptionException e) - { + } catch (OptionException e) { System.out.println("Usage: ConsoleMerger --strip --input --output --data ..."); e.printStackTrace(); } } + + + private static Set loadList(List strings, List files) throws IOException { + Set classes = new HashSet<>(); + classes.addAll(strings); + + for (File value : files) { + IMappingFile map = IMappingFile.load(value); + for (IClass cls : map.getClasses()) + classes.add(cls.getOriginal()); + } + + return classes; + } + + private static Set loadPackages(List strings) { + Set pkgs = new HashSet<>(); + for (String value : strings) { + if (!value.endsWith("/")) + value += '/'; + pkgs.add(value); + } + return pkgs; + } + + @SuppressWarnings("unchecked") + private static R sneak(Throwable e) throws E { + throw (E)e; + } } diff --git a/src/main/java/net/minecraftforge/mergetool/Merger.java b/src/main/java/net/minecraftforge/mergetool/Merger.java index e037ad1..60e7c35 100644 --- a/src/main/java/net/minecraftforge/mergetool/Merger.java +++ b/src/main/java/net/minecraftforge/mergetool/Merger.java @@ -1,20 +1,6 @@ /* - * MergeTool - * Copyright (c) 2016-2018. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only */ package net.minecraftforge.mergetool; @@ -28,15 +14,18 @@ import org.objectweb.asm.tree.MethodNode; import java.io.BufferedOutputStream; +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; 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.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; import java.util.List; @@ -46,13 +35,16 @@ import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; -@SuppressWarnings("unchecked") -public class Merger -{ +public class Merger { private static final boolean DEBUG = false; private final File client; @@ -63,117 +55,110 @@ public class Merger private FieldName FIELD = new FieldName(); private MethodDesc METHOD = new MethodDesc(); private HashSet whitelist = new HashSet<>(); + private Predicate filter = name -> this.whitelist.isEmpty() || this.whitelist.contains(name); private boolean copyData = false; private boolean keepMeta = false; + private boolean bundledServerJar = false; - public Merger(File client, File server, File merged) - { + public Merger(File client, File server, File merged) { this.client = client; this.server = server; this.merged = merged; } - public Merger annotate(AnnotationVersion ano, boolean inject) - { + public Merger annotate(AnnotationVersion ano, boolean inject) { this.annotation = ano; this.annotationInject = inject; return this; } - public Merger whitelist(String file) - { + public Merger whitelist(String file) { this.whitelist.add(file); return this; } - public Merger keepData() - { + /* + * The predicate is called with the class name in binary format {essentially the path to the class file, minus the .class suffix) + * Anything that does not return true from this predicate will be filtered out. + */ + public Merger filter(Predicate filter) { + this.filter = filter; + return this; + } + + public Merger keepData() { this.copyData = true; return this; } - public Merger skipData() - { + public Merger skipData() { this.copyData = false; return this; } - public Merger keepMeta() - { + public Merger keepMeta() { this.keepMeta = true; return this; } - public Merger skipMeta() - { + public Merger skipMeta() { this.keepMeta = false; return this; } - public void process() throws IOException - { - try ( - ZipFile cInJar = new ZipFile(this.client); - ZipFile sInJar = new ZipFile(this.server); - ZipOutputStream outJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(this.merged))) - ) { - Set added = new HashSet<>(); - Map cClasses = getClassEntries(cInJar, outJar, added); - Map sClasses = getClassEntries(sInJar, outJar, null); //Skip data from the server, as it contains libraries. + public Merger bundledServerJar() { + this.bundledServerJar = true; + return this; + } - for (Entry entry : cClasses.entrySet()) - { + public void process() throws IOException { + try (ZipOutputStream outJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(this.merged)))) { + Set added = new HashSet<>(); + Map cClasses = getClassEntries(this.client, outJar, added); + Map sClasses; + if (this.bundledServerJar) + sClasses = getBundledClassEntries(this.server, outJar, null); + else + sClasses = getClassEntries(this.server, outJar, null); //Skip data from the server, as it contains libraries. + + for (Entry entry : cClasses.entrySet()) { String name = entry.getKey(); - if (!this.whitelist.isEmpty() && !this.whitelist.contains(name)) + if (!this.filter.test(name)) continue; - ZipEntry cEntry = entry.getValue(); - ZipEntry sEntry = sClasses.get(name); + byte[] cData = entry.getValue(); + byte[] sData = sClasses.get(name); - if (sEntry == null) - { + if (sData == null) { if (DEBUG) - { System.out.println("Copy class c->s : " + name); - } - copyClass(cInJar, cEntry, outJar, true); - } - else - { - + copyClass(name, cData, outJar, true); + } else { if (DEBUG) - { System.out.println("Processing class: " + name); - } sClasses.remove(name); - byte[] cData = readEntry(cInJar, entry.getValue()); - byte[] sData = readEntry(sInJar, sEntry); byte[] data = processClass(cData, sData); - outJar.putNextEntry(getNewEntry(cEntry.getName())); + outJar.putNextEntry(getNewEntry(name + ".class")); outJar.write(data); } } - for (Entry entry : sClasses.entrySet()) - { - if (!this.whitelist.isEmpty() && !this.whitelist.contains(entry.getKey())) + for (Entry entry : sClasses.entrySet()) { + String name = entry.getKey(); + if (!this.filter.test(name)) continue; if (DEBUG) - { - System.out.println("Copy class s->c : " + entry.getKey()); - } - copyClass(sInJar, entry.getValue(), outJar, false); + System.out.println("Copy class s->c : " + name); + copyClass(name, entry.getValue(), outJar, false); } - if (this.annotation != null && this.annotationInject) - { - for (String cls : this.annotation.getClasses()) - { + if (this.annotation != null && this.annotationInject) { + for (String cls : this.annotation.getClasses()) { byte[] data = getResourceBytes(cls); outJar.putNextEntry(getNewEntry(cls + ".class")); @@ -184,16 +169,14 @@ public void process() throws IOException } } - private ZipEntry getNewEntry(String name) - { + private ZipEntry getNewEntry(String name) { ZipEntry ret = new ZipEntry(name); ret.setTime(0x92D6688800L); //Stabilize output as java will use current time if we don't set this, we can't use 0 as older java versions output different jars for values less then 1980 return ret; } - private void copyClass(ZipFile inJar, ZipEntry entry, ZipOutputStream outJar, boolean isClientOnly) throws IOException - { - ClassReader reader = new ClassReader(readEntry(inJar, entry)); + private void copyClass(String name, byte[] entry, ZipOutputStream outJar, boolean isClientOnly) throws IOException { + ClassReader reader = new ClassReader(entry); ClassNode classNode = new ClassNode(); reader.accept(classNode, 0); @@ -206,35 +189,35 @@ private void copyClass(ZipFile inJar, ZipEntry entry, ZipOutputStream outJar, bo byte[] data = writer.toByteArray(); - outJar.putNextEntry(getNewEntry(entry.getName())); + outJar.putNextEntry(getNewEntry(name + ".class")); outJar.write(data); } - private Map getClassEntries(ZipFile inFile, ZipOutputStream output, Set added) throws IOException - { - Map ret = new Hashtable(); - for (ZipEntry entry : Collections.list((Enumeration)inFile.entries())) - { + private Map getClassEntries(File inFile, ZipOutputStream output, Set added) throws IOException { + try (ZipInputStream zin = new ZipInputStream(new FileInputStream(inFile))) { + return getClassEntries(zin, output, added); + } + } + + private Map getClassEntries(ZipInputStream input, ZipOutputStream output, Set added) throws IOException { + Map ret = new Hashtable<>(); + for (ZipEntry entry; (entry = input.getNextEntry()) != null; ) { String entryName = entry.getName(); - if (!entry.isDirectory() && entryName.endsWith(".class") && !entryName.startsWith(".")) - { - ret.put(entryName.replace(".class", ""), entry); - } - else if (this.copyData && added != null && !added.contains(entryName)) - { + if (!entry.isDirectory() && entryName.endsWith(".class") && !entryName.startsWith(".")) { + entryName = entryName.substring(0, entryName.length() - 6); + byte[] data = readFully(input); + ret.put(entryName, data); + } else if (this.copyData && added != null && !added.contains(entryName)) { if (!this.keepMeta && entryName.startsWith("META-INF")) continue; - if (entry.isDirectory()) - { + if (entry.isDirectory()) { //Skip directories, they arnt required. //output.putNextEntry(getNewEntry(entryName)); //New entry to reset time added.add(entryName); - } - else - { + } else { output.putNextEntry(getNewEntry(entryName)); - output.write(readEntry(inFile, entry)); + copy(input, output); added.add(entryName); } } @@ -242,30 +225,72 @@ else if (this.copyData && added != null && !added.contains(entryName)) return ret; } - private byte[] readEntry(ZipFile inFile, ZipEntry entry) throws IOException - { - return readFully(inFile.getInputStream(entry)); + private static final Attributes.Name BUNDLER_FORMAT = new Attributes.Name("Bundler-Format"); + private static final String VERSIONS_LIST = "META-INF/versions.list"; + private Map getBundledClassEntries(File inFile, ZipOutputStream output, Set added) throws IOException { + try (ZipFile zin = new ZipFile(inFile)) { + ZipEntry mfEntry = zin.getEntry(JarFile.MANIFEST_NAME); + if (mfEntry == null) + throw new IOException("Invalid bundled server jar, Missing " + JarFile.MANIFEST_NAME); + + Manifest mf = new Manifest(zin.getInputStream(mfEntry)); + String format = mf.getMainAttributes().getValue(BUNDLER_FORMAT); + if (format == null) + throw new IOException("Invalid bundled server jar, Missing " + BUNDLER_FORMAT + " manifest entry"); + + if (!"1.0".equals(format)) + throw new IOException("Unsupported Bundler-Format: " + format); + + ZipEntry verEntry = zin.getEntry(VERSIONS_LIST); + if (verEntry == null) + throw new IllegalStateException("Bundled Jar missing " + VERSIONS_LIST); + + List verList = readList(zin.getInputStream(verEntry)); + if (verList.size() != 1) + throw new IllegalStateException("Invalid bundler " + VERSIONS_LIST + " file, " + verList.size() + " entries, expected 1"); + + String serverJarName = "META-INF/versions/" + verList.get(0).path; + ZipEntry serverJarEntry = zin.getEntry(serverJarName); + if (serverJarEntry == null) + throw new IOException("Invalid bundled server jar, Missing jar entry " + serverJarName); + + try (ZipInputStream server = new ZipInputStream(zin.getInputStream(serverJarEntry))) { + return getClassEntries(server, output, added); + } + } + } + + private static List readList(InputStream in) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + List ret = new ArrayList<>(); + while (reader.ready()) { + String line = reader.readLine(); + String[] pts = line.split("\t"); + if (pts.length != 3) + throw new IOException("Invalid bunder list line: " + line); + ret.add(new BundleEntry(pts[0], pts[1], pts[2])); + } + return ret; + } } - private byte[] readFully(InputStream stream) throws IOException - { - byte[] data = new byte[4096]; + private byte[] readFully(InputStream stream) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); + copy(stream, buf); + return buf.toByteArray(); + } + + private static void copy(InputStream input, OutputStream output) throws IOException { + byte[] data = new byte[4096]; int len; - do - { - len = stream.read(data); + do { + len = input.read(data); if (len > 0) - { - buf.write(data, 0, len); - } + output.write(data, 0, len); } while (len != -1); - - return buf.toByteArray(); } - private byte[] processClass(byte[] cIn, byte[] sIn) - { + private byte[] processClass(byte[] cIn, byte[] sIn) { ClassNode cClassNode = getClassNode(cIn); ClassNode sClassNode = getClassNode(sIn); @@ -279,70 +304,61 @@ private byte[] processClass(byte[] cIn, byte[] sIn) return writer.toByteArray(); } - private boolean innerMatches(InnerClassNode o, InnerClassNode o2) - { + private boolean innerMatches(InnerClassNode o, InnerClassNode o2) { return equals(o.innerName, o2.innerName) && equals(o.name, o2.name) && equals(o.outerName, o2.outerName); } - private boolean equals(Object o1, Object o2) - { + private boolean equals(Object o1, Object o2) { return o1 == null ? o2 == null : o2 == null ? false : o1.equals(o2); } - private void processInners(ClassNode cClass, ClassNode sClass) - { + private void processInners(ClassNode cClass, ClassNode sClass) { List cIners = cClass.innerClasses; List sIners = sClass.innerClasses; - for (InnerClassNode n : cIners) - { + for (InnerClassNode n : cIners) { if (!sIners.stream().anyMatch(e -> innerMatches(e, n))) sIners.add(n); } - for (InnerClassNode n : sIners) - { + + for (InnerClassNode n : sIners) { if (!cIners.stream().anyMatch(e -> innerMatches(e, n))) cIners.add(n); } } - private void processInterfaces(ClassNode cClass, ClassNode sClass) - { + private void processInterfaces(ClassNode cClass, ClassNode sClass) { List cIntfs = cClass.interfaces; List sIntfs = sClass.interfaces; List cOnly = new ArrayList<>(); List sOnly = new ArrayList<>(); - for (String n : cIntfs) - { - if (!sIntfs.contains(n)) - { + for (String n : cIntfs) { + if (!sIntfs.contains(n)) { sIntfs.add(n); cOnly.add(n); } } - for (String n : sIntfs) - { - if (!cIntfs.contains(n)) - { + + for (String n : sIntfs) { + if (!cIntfs.contains(n)) { cIntfs.add(n); sOnly.add(n); } } + Collections.sort(cIntfs); //Sort things, we're in obf territory but should stabilize things. Collections.sort(sIntfs); - if (this.annotation != null && !cOnly.isEmpty() || !sOnly.isEmpty()) - { + if (this.annotation != null && !cOnly.isEmpty() || !sOnly.isEmpty()) { this.annotation.add(cClass, cOnly, sOnly); this.annotation.add(sClass, cOnly, sOnly); } } - private ClassNode getClassNode(byte[] data) - { + private ClassNode getClassNode(byte[] data) { ClassReader reader = new ClassReader(data); ClassNode classNode = new ClassNode(); reader.accept(classNode, 0); @@ -353,38 +369,31 @@ private ClassNode getClassNode(byte[] data) return (a,b) -> primary.test(function.apply(a), function.apply(b)); } - private void processFields(ClassNode cClass, ClassNode sClass) - { + private void processFields(ClassNode cClass, ClassNode sClass) { merge(cClass.name, sClass.name, cClass.fields, sClass.fields, curry(FIELD, Objects::equals), FIELD, FIELD, FIELD); } - private void processMethods(ClassNode cClass, ClassNode sClass) - { + private void processMethods(ClassNode cClass, ClassNode sClass) { merge(cClass.name, sClass.name, cClass.methods, sClass.methods, curry(METHOD, Objects::equals), METHOD, METHOD, METHOD); } - private interface MemberAnnotator - { + private interface MemberAnnotator { T process(T member, boolean isClient); } - private class FieldName implements Function, MemberAnnotator, Comparator - { - public String apply(FieldNode in) - { + private class FieldName implements Function, MemberAnnotator, Comparator { + public String apply(FieldNode in) { return in == null ? "null" : in.name; } - public FieldNode process(FieldNode field, boolean isClient) - { + public FieldNode process(FieldNode field, boolean isClient) { if (Merger.this.annotation != null) Merger.this.annotation.add(field, isClient); return field; } @Override - public int compare(FieldNode a, FieldNode b) - { + public int compare(FieldNode a, FieldNode b) { if (a == b) return 0; if (a == null) return 1; if (b == null) return -1; @@ -392,36 +401,28 @@ public int compare(FieldNode a, FieldNode b) } } - private class MethodDesc implements Function, MemberAnnotator, Comparator - { - public String apply(MethodNode node) - { + private class MethodDesc implements Function, MemberAnnotator, Comparator { + public String apply(MethodNode node) { return node == null ? "null" : node.name + node.desc; } - public MethodNode process(MethodNode node, boolean isClient) - { + public MethodNode process(MethodNode node, boolean isClient) { if (Merger.this.annotation != null) Merger.this.annotation.add(node, isClient); return node; } - private int findLine(MethodNode member) - { - for (int x = 0; x < member.instructions.size(); x++) - { + private int findLine(MethodNode member) { + for (int x = 0; x < member.instructions.size(); x++) { AbstractInsnNode insn = member.instructions.get(x); if (insn instanceof LineNumberNode) - { return ((LineNumberNode)insn).line; - } } return Integer.MAX_VALUE; } @Override - public int compare(MethodNode a, MethodNode b) - { + public int compare(MethodNode a, MethodNode b) { if (a == b) return 0; if (a == null) return 1; if (b == null) return -1; @@ -430,18 +431,14 @@ public int compare(MethodNode a, MethodNode b) } private void merge(String cName, String sName, List client, List server, BiPredicate eq, - MemberAnnotator annotator, Function toString, Comparator compare) - { + MemberAnnotator annotator, Function toString, Comparator compare) { // adding null to the end to not handle the index overflow in a special way client.add(null); server.add(null); List common = new ArrayList<>(); - for(T ct : client) - { - for (T st : server) - { - if (eq.test(ct, st)) - { + for(T ct : client) { + for (T st : server) { + if (eq.test(ct, st)) { common.add(ct); break; } @@ -449,61 +446,49 @@ private void merge(String cName, String sName, List client, List serve } int i = 0, mi = 0; - for(; i < client.size(); i++) - { + for(; i < client.size(); i++) { T ct = client.get(i); T st = server.get(i); T mt = common.get(mi); - if (eq.test(ct, st)) - { + if (eq.test(ct, st)) { mi++; if (!eq.test(ct, mt)) throw new IllegalStateException("merged list is in bad state: " + toString.apply(ct) + " " + toString.apply(st) + " " + toString.apply(mt)); if (DEBUG) System.out.printf("%d/%d %d/%d Both Shared : %s %s\n", i, client.size(), mi, common.size(), sName, toString.apply(st)); - } - else if(eq.test(st, mt)) - { + } else if(eq.test(st, mt)) { server.add(i, annotator.process(ct, true)); if (DEBUG) System.out.printf("%d/%d %d/%d Server *add* : %s %s\n", i, client.size(), mi, common.size(), sName, toString.apply(ct)); - } - else if (eq.test(ct, mt)) - { + } else if (eq.test(ct, mt)) { client.add(i, annotator.process(st, false)); if (DEBUG) System.out.printf("%d/%d %d/%d Client *add* : %s %s\n", i, client.size(), mi, common.size(), cName, toString.apply(st)); - } - else // Both server and client add a new method before we get to the next common method... Lets try and prioritize one. - { + } else { // Both server and client add a new method before we get to the next common method... Lets try and prioritize one. int diff = compare.compare(ct, st); - if (diff > 0) - { + if (diff > 0) { client.add(i, annotator.process(st, false)); if (DEBUG) System.out.printf("%d/%d %d/%d Client *add* : %s %s\n", i, client.size(), mi, common.size(), cName, toString.apply(st)); - } - else /* if (diff < 0) */ //Technically this should be <0 and we special case when they can't agree who goes first.. but for now just push the client's first. - { + } else { /* if (diff < 0) */ //Technically this should be <0 and we special case when they can't agree who goes first.. but for now just push the client's first. server.add(i, annotator.process(ct, true)); if (DEBUG) System.out.printf("%d/%d %d/%d Server *add* : %s %s\n", i, client.size(), mi, common.size(), sName, toString.apply(ct)); } } } + if (i < server.size() || mi < common.size() || (client.size() != server.size())) - { throw new IllegalStateException("merged list is in bad state: " + i + " " + mi); - } + // removing the null client.remove(client.size() - 1); server.remove(server.size() - 1); } - private byte[] getResourceBytes(String path) throws IOException - { + private byte[] getResourceBytes(String path) throws IOException { // If we're in the built jar, use the relocated classes {prevents them being InputStream stream = Merger.class.getResourceAsStream("/markers/" + path + ".marker"); // If not, then try and get them from the classpath @@ -511,10 +496,25 @@ private byte[] getResourceBytes(String path) throws IOException stream = Merger.class.getResourceAsStream("/" + path + ".class"); if (stream == null) throw new IllegalStateException("Could not find marker files: " + path); + try { return readFully(stream); } finally { stream.close(); } } + + + @SuppressWarnings("unused") + private static class BundleEntry { + public final String hash; + public final String artifact; + public final String path; + + private BundleEntry(String hash, String artifact, String path) { + this.hash = hash; + this.artifact = artifact; + this.path = path; + } + } } diff --git a/src/main/java/net/minecraftforge/mergetool/Stripper.java b/src/main/java/net/minecraftforge/mergetool/Stripper.java index 4341036..78c13c8 100644 --- a/src/main/java/net/minecraftforge/mergetool/Stripper.java +++ b/src/main/java/net/minecraftforge/mergetool/Stripper.java @@ -1,20 +1,6 @@ /* - * MergeTool - * Copyright (c) 2016-2018. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only */ package net.minecraftforge.mergetool; @@ -35,15 +21,19 @@ import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; -public class Stripper -{ +public class Stripper { private Set classes = new HashSet<>(); private Set targets = new HashSet<>(); - public void loadData(File file) throws IOException - { - Files.lines(file.toPath()).forEach(line -> - { + /* + * Data files list whole classes, or methods that should be stripped out. + * Comments are supported, anything following the # character will be stripped + * Empty lines are ignored. + * If the line starts with \t the \t will be stripped + * You can strip annotations from classes, or methods + */ + public void loadData(File file) throws IOException { + Files.lines(file.toPath()).forEach(line -> { int idx = line.indexOf('#'); if (idx == 0 || line.isEmpty()) return; if (idx != -1) line = line.substring(0, idx - 1); @@ -57,53 +47,41 @@ public void loadData(File file) throws IOException }); } - public void process(File input, File output) throws IOException - { + public void process(File input, File output) throws IOException { if (output.exists()) output.delete(); if (!output.getParentFile().exists()) output.getParentFile().mkdirs(); output.createNewFile(); Set types = new HashSet<>(); - for (AnnotationVersion an : AnnotationVersion.values()) - { + for (AnnotationVersion an : AnnotationVersion.values()) { for (String cls : an.getClasses()) types.add('L' + cls + ';'); } try (ZipInputStream zis = new ZipInputStream(new FileInputStream(input)); - ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(output))) - { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(output))) { ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) - { + while ((entry = zis.getNextEntry()) != null) { ZipEntry next = new ZipEntry(entry.getName()); next.setTime(entry.getTime()); next.setLastModifiedTime(entry.getLastModifiedTime()); zos.putNextEntry(next); - if (!entry.getName().endsWith(".class") || !classes.contains(entry.getName().substring(0, entry.getName().length() - 6))) - { + if (!entry.getName().endsWith(".class") || !classes.contains(entry.getName().substring(0, entry.getName().length() - 6))) { int read; byte[] buf = new byte[0x100]; while ((read = zis.read(buf, 0, buf.length)) != -1) zos.write(buf, 0, read); - } - else - { + } else { ClassReader reader = new ClassReader(zis); ClassNode node = new ClassNode(); reader.accept(node, 0); - if (node.methods != null) - { - node.methods.forEach(mtd -> - { - if (targets.contains(node.name + ' ' + mtd.name + mtd.desc)) - { - if (mtd.visibleAnnotations != null) - { + if (node.methods != null) { + node.methods.forEach(mtd -> { + if (targets.contains(node.name + ' ' + mtd.name + mtd.desc)) { + if (mtd.visibleAnnotations != null) { Iterator itr = mtd.visibleAnnotations.iterator(); - while (itr.hasNext()) - { + while (itr.hasNext()) { if (types.contains(itr.next().desc)) itr.remove(); } @@ -112,11 +90,9 @@ public void process(File input, File output) throws IOException }); } - if (node.visibleAnnotations != null) - { + if (node.visibleAnnotations != null) { Iterator itr = node.visibleAnnotations.iterator(); - while (itr.hasNext()) - { + while (itr.hasNext()) { if (types.contains(itr.next().desc)) itr.remove(); }