From 0ae8535c40f730049cef7f60aef1b222353baf96 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Mon, 6 Apr 2020 15:28:53 +0100 Subject: [PATCH] Access widener support (#182) * Rough work on project based jars, skeleton for AccessEscalators? * First working draft * Minor changes * Add support for mutable, better error checking when parsing file. Code cleanup Remap if needed when reading * Fix inner classes and genSources * Fix CME * Caching, only regen jar when input changes * Some work, untested * Fix writing, fix checkstyle issues * More fixes * Move jars into a maven file structure, cleans up the file structure, and will benefit idea 2020 Add some basic validation to the AccessWidenerRemapper, will present any issues with the mappings when building (May need a way to disable?) + Some bugs fixes * Fix issues with source jars in idea 2020, should be backwards compatible with 2019 * Move to lorenz-tiny * Build fix + small cleanup * Update to match the changes in loader * More fixes * Update to match loader changes. * Improve error logging --- .../loom/processors/JarProcessorManager.java | 5 + .../net/fabricmc/loom/task/RemapJarTask.java | 5 + .../net/fabricmc/loom/util/ModProcessor.java | 59 +++ .../util/accesswidener/AccessWidener.java | 457 ++++++++++++++++++ .../AccessWidenerJarProcessor.java | 240 +++++++++ .../accesswidener/AccessWidenerRemapper.java | 110 +++++ 6 files changed, 876 insertions(+) create mode 100644 src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidener.java create mode 100644 src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerJarProcessor.java create mode 100644 src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerRemapper.java diff --git a/src/main/java/net/fabricmc/loom/processors/JarProcessorManager.java b/src/main/java/net/fabricmc/loom/processors/JarProcessorManager.java index b71efda..579296c 100644 --- a/src/main/java/net/fabricmc/loom/processors/JarProcessorManager.java +++ b/src/main/java/net/fabricmc/loom/processors/JarProcessorManager.java @@ -31,6 +31,7 @@ import java.util.List; import org.gradle.api.Project; +import net.fabricmc.loom.util.accesswidener.AccessWidenerJarProcessor; import net.fabricmc.loom.LoomGradleExtension; public class JarProcessorManager { @@ -49,6 +50,10 @@ public class JarProcessorManager { private List setupProcessors() { List jarProcessors = new ArrayList<>(); + if (extension.accessWidener != null) { + jarProcessors.add(new AccessWidenerJarProcessor()); + } + jarProcessors.forEach(jarProcessor -> jarProcessor.setup(project)); return Collections.unmodifiableList(jarProcessors); } diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index 446702f..33c6b70 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -45,6 +45,7 @@ import net.fabricmc.loom.util.GradleSupport; import net.fabricmc.loom.util.MixinRefmapHelper; import net.fabricmc.loom.util.NestedJars; import net.fabricmc.loom.util.TinyRemapperMappingsHelper; +import net.fabricmc.loom.util.accesswidener.AccessWidenerJarProcessor; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; import net.fabricmc.tinyremapper.TinyUtils; @@ -128,6 +129,10 @@ public class RemapJarTask extends Jar { } } + if (extension.accessWidener != null) { + extension.getJarProcessorManager().getByType(AccessWidenerJarProcessor.class).remapAccessWidener(output); + } + /*try { if (modJar.exists()) { Files.move(modJar, modJarUnmappedCopy); diff --git a/src/main/java/net/fabricmc/loom/util/ModProcessor.java b/src/main/java/net/fabricmc/loom/util/ModProcessor.java index 07401ec..e339d4a 100644 --- a/src/main/java/net/fabricmc/loom/util/ModProcessor.java +++ b/src/main/java/net/fabricmc/loom/util/ModProcessor.java @@ -24,10 +24,13 @@ package net.fabricmc.loom.util; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; @@ -47,12 +50,15 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.ResolvedArtifact; import org.zeroturnaround.zip.ZipUtil; import org.zeroturnaround.zip.commons.FileUtils; +import org.zeroturnaround.zip.transform.ByteArrayZipEntryTransformer; import org.zeroturnaround.zip.transform.StringZipEntryTransformer; import org.zeroturnaround.zip.transform.ZipEntryTransformerEntry; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.providers.MappingsProvider; import net.fabricmc.loom.providers.MinecraftMappedProvider; +import net.fabricmc.loom.util.accesswidener.AccessWidener; +import net.fabricmc.loom.util.accesswidener.AccessWidenerRemapper; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; @@ -71,6 +77,8 @@ public class ModProcessor { handleNestedJars(input, project, config, artifact); } + remapaccessWidener(input, project); + //Always strip the nested jars stripNestedJars(output); } @@ -140,6 +148,57 @@ public class ModProcessor { }))}); } + private static void remapaccessWidener(File input, Project project) throws IOException { + JarFile jarFile = new JarFile(input); + JarEntry modJsonEntry = jarFile.getJarEntry("fabric.mod.json"); + + if (modJsonEntry == null) { + return; + } + + String accessWidenerPath; + + try (InputStream inputStream = jarFile.getInputStream(modJsonEntry)) { + JsonObject json = GSON.fromJson(new InputStreamReader(inputStream), JsonObject.class); + + if (!json.has("accessWidener")) { + return; + } + + accessWidenerPath = json.get("accessWidener").getAsString(); + } + + if (accessWidenerPath == null) { + return; + } + + ZipUtil.transformEntry(input, accessWidenerPath, new ByteArrayZipEntryTransformer() { + @Override + protected byte[] transform(ZipEntry zipEntry, byte[] input) throws IOException { + return remapaccessWidener(input, project); + } + }); + } + + private static byte[] remapaccessWidener(byte[] input, Project project) { + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input)))) { + AccessWidener accessWidener = new AccessWidener(); + accessWidener.read(bufferedReader); + + AccessWidenerRemapper accessWidenerRemapper = new AccessWidenerRemapper(accessWidener, project.getExtensions().getByType(LoomGradleExtension.class).getMappingsProvider().getMappings(), "named"); + AccessWidener remapped = accessWidenerRemapper.remap(); + + StringWriter writer = new StringWriter(); + remapped.write(writer); + byte[] bytes = writer.toString().getBytes(); + writer.close(); + + return bytes; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + private static void remapJar(File input, File output, Project project, ResolvedArtifact artifact) throws IOException { LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); String fromM = "intermediary"; diff --git a/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidener.java b/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidener.java new file mode 100644 index 0000000..b44011a --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidener.java @@ -0,0 +1,457 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.accesswidener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.IntUnaryOperator; + +import org.objectweb.asm.Opcodes; + +import net.fabricmc.mappings.EntryTriple; + +public class AccessWidener { + public String namespace; + public Map classAccess = new HashMap<>(); + public Map methodAccess = new HashMap<>(); + public Map fieldAccess = new HashMap<>(); + private Set classes = new LinkedHashSet<>(); + + public void read(BufferedReader reader) throws IOException { + String[] header = reader.readLine().split("\\s+"); + + if (header.length != 3 || !header[0].equals("accessWidener")) { + throw new UnsupportedOperationException("Invalid access access widener header"); + } + + if (!header[1].equals("v1")) { + throw new RuntimeException(String.format("Unsupported access widener format (%s)", header[1])); + } + + if (namespace != null) { + if (!namespace.equals(header[2])) { + throw new RuntimeException(String.format("Namespace mismatch, expected %s got %s", namespace, header[2])); + } + } + + namespace = header[2]; + + String line; + + Set targets = new LinkedHashSet<>(); + + while ((line = reader.readLine()) != null) { + //Comment handling + int commentPos = line.indexOf('#'); + + if (commentPos >= 0) { + line = line.substring(0, commentPos).trim(); + } + + if (line.isEmpty()) continue; + + String[] split = line.split("\\s+"); + + if (split.length != 3 && split.length != 5) { + throw new RuntimeException(String.format("Invalid line (%s)", line)); + } + + String access = split[0]; + + targets.add(split[2].replaceAll("/", ".")); + + switch (split[1]) { + case "class": + if (split.length != 3) { + throw new RuntimeException(String.format("Expected (\tclass\t) got (%s)", line)); + } + + classAccess.put(split[2], applyAccess(access, classAccess.getOrDefault(split[2], ClassAccess.DEFAULT), null)); + break; + case "field": + if (split.length != 5) { + throw new RuntimeException(String.format("Expected (\tfield\t\t\t) got (%s)", line)); + } + + addOrMerge(fieldAccess, new EntryTriple(split[2], split[3], split[4]), access, FieldAccess.DEFAULT); + break; + case "method": + if (split.length != 5) { + throw new RuntimeException(String.format("Expected (\tmethod\t\t\t) got (%s)", line)); + } + + addOrMerge(methodAccess, new EntryTriple(split[2], split[3], split[4]), access, MethodAccess.DEFAULT); + break; + default: + throw new UnsupportedOperationException("Unsupported type " + split[1]); + } + } + + Set parentClasses = new LinkedHashSet<>(); + + //Also transform all parent classes + for (String clazz : targets) { + while (clazz.contains("$")) { + clazz = clazz.substring(0, clazz.lastIndexOf("$")); + parentClasses.add(clazz); + } + } + + classes.addAll(targets); + classes.addAll(parentClasses); + } + + //Could possibly be cleaner but should do its job for now + public void write(StringWriter writer) { + writer.write("accessWidener\tv1\t"); + writer.write(namespace); + writer.write("\n"); + + for (Map.Entry entry : classAccess.entrySet()) { + for (String s : getAccesses(entry.getValue())) { + writer.write(s); + writer.write("\tclass\t"); + writer.write(entry.getKey()); + writer.write("\n"); + } + } + + for (Map.Entry entry : methodAccess.entrySet()) { + writeEntry(writer, "method", entry.getKey(), entry.getValue()); + } + + for (Map.Entry entry : fieldAccess.entrySet()) { + writeEntry(writer, "field", entry.getKey(), entry.getValue()); + } + } + + private void writeEntry(StringWriter writer, String type, EntryTriple entryTriple, Access access) { + for (String s : getAccesses(access)) { + writer.write(s); + writer.write("\t"); + writer.write(type); + writer.write("\t"); + writer.write(entryTriple.getOwner()); + writer.write("\t"); + writer.write(entryTriple.getName()); + writer.write("\t"); + writer.write(entryTriple.getDesc()); + writer.write("\n"); + } + } + + private List getAccesses(Access access) { + List accesses = new ArrayList<>(); + + if (access == ClassAccess.ACCESSIBLE || access == MethodAccess.ACCESSIBLE || access == FieldAccess.ACCESSIBLE || access == MethodAccess.ACCESSIBLE_EXTENDABLE || access == ClassAccess.ACCESSIBLE_EXTENDABLE || access == FieldAccess.ACCESSIBLE_MUTABLE) { + accesses.add("accessible"); + } + + if (access == ClassAccess.EXTENDABLE || access == MethodAccess.EXTENDABLE || access == MethodAccess.ACCESSIBLE_EXTENDABLE || access == ClassAccess.ACCESSIBLE_EXTENDABLE) { + accesses.add("extendable"); + } + + if (access == FieldAccess.MUTABLE || access == FieldAccess.ACCESSIBLE_MUTABLE) { + accesses.add("mutable"); + } + + return accesses; + } + + void addOrMerge(Map map, EntryTriple entry, Access access) { + if (entry == null || access == null) { + throw new RuntimeException("Input entry or access is null"); + } + + Access merged = null; + + if (access instanceof ClassAccess) { + merged = ClassAccess.DEFAULT; + } else if (access instanceof MethodAccess) { + merged = MethodAccess.DEFAULT; + } else if (access instanceof FieldAccess) { + merged = FieldAccess.DEFAULT; + } + + merged = mergeAccess(merged, access); + + map.put(entry, merged); + } + + void addOrMerge(Map map, EntryTriple entry, String access, Access defaultAccess) { + if (entry == null || access == null) { + throw new RuntimeException("Input entry or access is null"); + } + + map.put(entry, applyAccess(access, map.getOrDefault(entry, defaultAccess), entry)); + } + + public void merge(AccessWidener other) { + if (namespace == null) { + namespace = other.namespace; + } else if (!namespace.equals(other.namespace)) { + throw new RuntimeException("Namespace mismatch"); + } + + for (Map.Entry entry : other.classAccess.entrySet()) { + if (classAccess.containsKey(entry.getKey())) { + classAccess.replace(entry.getKey(), mergeAccess(classAccess.get(entry.getKey()), entry.getValue())); + } else { + classAccess.put(entry.getKey(), entry.getValue()); + } + } + + for (Map.Entry entry : other.methodAccess.entrySet()) { + addOrMerge(methodAccess, entry.getKey(), entry.getValue()); + } + + for (Map.Entry entry : other.fieldAccess.entrySet()) { + addOrMerge(fieldAccess, entry.getKey(), entry.getValue()); + } + } + + private Access applyAccess(String input, Access access, EntryTriple entryTriple) { + switch (input.toLowerCase(Locale.ROOT)) { + case "accessible": + makeClassAccessible(entryTriple); + return access.makeAccessible(); + case "extendable": + makeClassExtendable(entryTriple); + return access.makeExtendable(); + case "mutable": + return access.makeMutable(); + default: + throw new UnsupportedOperationException("Unknown access type:" + input); + } + } + + private void makeClassAccessible(EntryTriple entryTriple) { + if (entryTriple == null) return; + classAccess.put(entryTriple.getOwner(), applyAccess("accessible", classAccess.getOrDefault(entryTriple.getOwner(), ClassAccess.DEFAULT), null)); + } + + private void makeClassExtendable(EntryTriple entryTriple) { + if (entryTriple == null) return; + classAccess.put(entryTriple.getOwner(), applyAccess("extendable", classAccess.getOrDefault(entryTriple.getOwner(), ClassAccess.DEFAULT), null)); + } + + private static Access mergeAccess(Access a, Access b) { + Access access = a; + + if (b == ClassAccess.ACCESSIBLE || b == MethodAccess.ACCESSIBLE || b == FieldAccess.ACCESSIBLE || b == MethodAccess.ACCESSIBLE_EXTENDABLE || b == ClassAccess.ACCESSIBLE_EXTENDABLE || b == FieldAccess.ACCESSIBLE_MUTABLE) { + access = access.makeAccessible(); + } + + if (b == ClassAccess.EXTENDABLE || b == MethodAccess.EXTENDABLE || b == MethodAccess.ACCESSIBLE_EXTENDABLE || b == ClassAccess.ACCESSIBLE_EXTENDABLE) { + access = access.makeExtendable(); + } + + if (b == FieldAccess.MUTABLE || b == FieldAccess.ACCESSIBLE_MUTABLE) { + access = access.makeMutable(); + } + + return access; + } + + public Access getClassAccess(String className) { + return classAccess.getOrDefault(className, ClassAccess.DEFAULT); + } + + public Access getFieldAccess(EntryTriple entryTriple) { + return fieldAccess.getOrDefault(entryTriple, FieldAccess.DEFAULT); + } + + public Access getMethodAccess(EntryTriple entryTriple) { + return methodAccess.getOrDefault(entryTriple, MethodAccess.DEFAULT); + } + + public Set getTargets() { + return classes; + } + + private static int makePublic(int i) { + return (i & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) | Opcodes.ACC_PUBLIC; + } + + private static int makeProtected(int i) { + if ((i & Opcodes.ACC_PUBLIC) != 0) { + //Return i if public + return i; + } + + return (i & ~(Opcodes.ACC_PRIVATE)) | Opcodes.ACC_PROTECTED; + } + + private static int makeFinalIfPrivate(int i) { + if ((i & Opcodes.ACC_PRIVATE) != 0) { + return i | Opcodes.ACC_FINAL; + } + + return i; + } + + private static int removeFinal(int i) { + return i & ~Opcodes.ACC_FINAL; + } + + public interface Access extends IntUnaryOperator { + Access makeAccessible(); + + Access makeExtendable(); + + Access makeMutable(); + } + + public enum ClassAccess implements Access { + DEFAULT(i -> i), + ACCESSIBLE(i -> makePublic(i)), + EXTENDABLE(i -> makePublic(removeFinal(i))), + ACCESSIBLE_EXTENDABLE(i -> makePublic(removeFinal(i))); + + private final IntUnaryOperator operator; + + ClassAccess(IntUnaryOperator operator) { + this.operator = operator; + } + + @Override + public Access makeAccessible() { + if (this == EXTENDABLE || this == ACCESSIBLE_EXTENDABLE) { + return ACCESSIBLE_EXTENDABLE; + } + + return ACCESSIBLE; + } + + @Override + public Access makeExtendable() { + if (this == ACCESSIBLE || this == ACCESSIBLE_EXTENDABLE) { + return ACCESSIBLE_EXTENDABLE; + } + + return EXTENDABLE; + } + + @Override + public Access makeMutable() { + throw new UnsupportedOperationException("Classes cannot be made mutable"); + } + + @Override + public int applyAsInt(int operand) { + return operator.applyAsInt(operand); + } + } + + public enum MethodAccess implements Access { + DEFAULT(i -> i), + ACCESSIBLE(i -> makePublic(makeFinalIfPrivate(i))), + EXTENDABLE(i -> makeProtected(removeFinal(i))), + ACCESSIBLE_EXTENDABLE(i -> makePublic(removeFinal(i))); + + private final IntUnaryOperator operator; + + MethodAccess(IntUnaryOperator operator) { + this.operator = operator; + } + + @Override + public Access makeAccessible() { + if (this == EXTENDABLE || this == ACCESSIBLE_EXTENDABLE) { + return ACCESSIBLE_EXTENDABLE; + } + + return ACCESSIBLE; + } + + @Override + public Access makeExtendable() { + if (this == ACCESSIBLE || this == ACCESSIBLE_EXTENDABLE) { + return ACCESSIBLE_EXTENDABLE; + } + + return EXTENDABLE; + } + + @Override + public Access makeMutable() { + throw new UnsupportedOperationException("Methods cannot be made mutable"); + } + + @Override + public int applyAsInt(int operand) { + return operator.applyAsInt(operand); + } + } + + public enum FieldAccess implements Access { + DEFAULT(i -> i), + ACCESSIBLE(i -> makePublic(i)), + MUTABLE(i -> removeFinal(i)), + ACCESSIBLE_MUTABLE(i -> makePublic(removeFinal(i))); + + private final IntUnaryOperator operator; + + FieldAccess(IntUnaryOperator operator) { + this.operator = operator; + } + + @Override + public Access makeAccessible() { + if (this == MUTABLE || this == ACCESSIBLE_MUTABLE) { + return ACCESSIBLE_MUTABLE; + } + + return ACCESSIBLE; + } + + @Override + public Access makeExtendable() { + throw new UnsupportedOperationException("Fields cannot be made extendable"); + } + + @Override + public Access makeMutable() { + if (this == ACCESSIBLE || this == ACCESSIBLE_MUTABLE) { + return ACCESSIBLE_MUTABLE; + } + + return MUTABLE; + } + + @Override + public int applyAsInt(int operand) { + return operator.applyAsInt(operand); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerJarProcessor.java b/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerJarProcessor.java new file mode 100644 index 0000000..baf07a5 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerJarProcessor.java @@ -0,0 +1,240 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.accesswidener; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Set; +import java.util.zip.ZipEntry; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.gradle.api.Project; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.zeroturnaround.zip.ZipUtil; +import org.zeroturnaround.zip.transform.ByteArrayZipEntryTransformer; +import org.zeroturnaround.zip.transform.ZipEntryTransformer; +import org.zeroturnaround.zip.transform.ZipEntryTransformerEntry; + +import net.fabricmc.mappings.EntryTriple; +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.util.Checksum; +import net.fabricmc.loom.processors.JarProcessor; + +public class AccessWidenerJarProcessor implements JarProcessor { + private AccessWidener accessWidener = new AccessWidener(); + private Project project; + private byte[] inputHash; + + @Override + public void setup(Project project) { + this.project = project; + LoomGradleExtension loomGradleExtension = project.getExtensions().getByType(LoomGradleExtension.class); + + if (!loomGradleExtension.accessWidener.exists()) { + throw new RuntimeException("Could not find access widener file @ " + loomGradleExtension.accessWidener.getAbsolutePath()); + } + + inputHash = Checksum.sha256(loomGradleExtension.accessWidener); + + try (BufferedReader reader = new BufferedReader(new FileReader(loomGradleExtension.accessWidener))) { + accessWidener.read(reader); + } catch (IOException e) { + throw new RuntimeException("Failed to read project access widener file"); + } + + //Remap accessWidener if its not named, allows for AE's to be written in intermediary + if (!accessWidener.namespace.equals("named")) { + try { + AccessWidenerRemapper remapper = new AccessWidenerRemapper(accessWidener, loomGradleExtension.getMappingsProvider().getMappings(), "named"); + accessWidener = remapper.remap(); + } catch (IOException e) { + throw new RuntimeException("Failed to remap access widener", e); + } + } + } + + @Override + public void process(File file) { + project.getLogger().lifecycle("Processing file: " + file.getName()); + ZipUtil.transformEntries(file, getTransformers(accessWidener.getTargets())); + ZipUtil.addEntry(file, "aw.sha256", inputHash); + } + + private ZipEntryTransformerEntry[] getTransformers(Set classes) { + return classes.stream() + .map(string -> new ZipEntryTransformerEntry(string.replaceAll("\\.", "/") + ".class", getTransformer(string))) + .toArray(ZipEntryTransformerEntry[]::new); + } + + private ZipEntryTransformer getTransformer(String className) { + return new ByteArrayZipEntryTransformer() { + @Override + protected byte[] transform(ZipEntry zipEntry, byte[] input) { + ClassReader reader = new ClassReader(input); + ClassWriter writer = new ClassWriter(0); + + project.getLogger().lifecycle("Applying access widener to " + className); + + reader.accept(new AccessTransformer(writer), 0); + return writer.toByteArray(); + } + }; + } + + //Called when remapping the mod + public void remapAccessWidener(Path modJarPath) throws IOException { + LoomGradleExtension extension = project.getExtensions().getByType(LoomGradleExtension.class); + AccessWidenerRemapper remapper = new AccessWidenerRemapper(accessWidener, extension.getMappingsProvider().getMappings(), "intermediary"); + AccessWidener remapped = remapper.remap(); + + StringWriter writer = new StringWriter(); + remapped.write(writer); + byte[] bytes = writer.toString().getBytes(); + writer.close(); + + String path = getAccessWidenerPath(modJarPath); + + if (path == null) { + return; + } + + boolean replaced = ZipUtil.replaceEntry(modJarPath.toFile(), path, bytes); + + if (!replaced) { + project.getLogger().warn("Failed to replace access widener file at " + path); + } + } + + private String getAccessWidenerPath(Path modJarPath) { + byte[] modJsonBytes = ZipUtil.unpackEntry(modJarPath.toFile(), "fabric.mod.json"); + + if (modJsonBytes == null) { + return null; + } + + JsonObject jsonObject = new Gson().fromJson(new String(modJsonBytes, StandardCharsets.UTF_8), JsonObject.class); + + if (!jsonObject.has("accessWidener")) { + return null; + } + + return jsonObject.get("accessWidener").getAsString(); + } + + @Override + public boolean isInvalid(File file) { + byte[] hash = ZipUtil.unpackEntry(file, "aw.sha256"); + + if (hash == null) { + return true; + } + + return !Arrays.equals(inputHash, hash); //TODO how do we know if the current jar as the correct access applied? save the hash of the input? + } + + private class AccessTransformer extends ClassVisitor { + private String className; + + private AccessTransformer(ClassVisitor classVisitor) { + super(Opcodes.ASM7, classVisitor); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + className = name; + super.visit( + version, + accessWidener.getClassAccess(name).applyAsInt(access), + name, + signature, + superName, + interfaces + ); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + super.visitInnerClass( + name, + outerName, + innerName, + accessWidener.getClassAccess(name).applyAsInt(access) + ); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + return super.visitField( + accessWidener.getFieldAccess(new EntryTriple(className, name, descriptor)).applyAsInt(access), + name, + descriptor, + signature, + value + ); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new AccessWidenerMethodVisitor(super.visitMethod( + accessWidener.getMethodAccess(new EntryTriple(className, name, descriptor)).applyAsInt(access), + name, + descriptor, + signature, + exceptions + )); + } + + private class AccessWidenerMethodVisitor extends MethodVisitor { + AccessWidenerMethodVisitor(MethodVisitor methodVisitor) { + super(Opcodes.ASM7, methodVisitor); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { + if (opcode == Opcodes.INVOKESPECIAL && owner.equals(className) && !name.equals("")) { + AccessWidener.Access methodAccess = accessWidener.getMethodAccess(new EntryTriple(owner, name, descriptor)); + + if (methodAccess != AccessWidener.MethodAccess.DEFAULT) { + opcode = Opcodes.INVOKEVIRTUAL; + } + } + + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerRemapper.java b/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerRemapper.java new file mode 100644 index 0000000..cda83d6 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/accesswidener/AccessWidenerRemapper.java @@ -0,0 +1,110 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2016, 2017, 2018 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.accesswidener; + +import java.util.HashMap; +import java.util.Map; + +import net.fabricmc.mapping.tree.ClassDef; +import net.fabricmc.mapping.tree.FieldDef; +import net.fabricmc.mapping.tree.MethodDef; +import net.fabricmc.mapping.tree.TinyTree; +import net.fabricmc.mappings.EntryTriple; + +public class AccessWidenerRemapper { + private final AccessWidener input; + private final String from, to; + + private Map classNames = new HashMap<>(); + private Map fieldNames = new HashMap<>(); + private Map methodNames = new HashMap<>(); + + public AccessWidenerRemapper(AccessWidener input, TinyTree tinyTree, String to) { + this.input = input; + this.from = input.namespace; + this.to = to; + populateMappings(tinyTree); + } + + private void populateMappings(TinyTree tinyTree) { + if (!tinyTree.getMetadata().getNamespaces().contains(from)) { + throw new UnsupportedOperationException("Unknown namespace: " + from); + } + + if (!tinyTree.getMetadata().getNamespaces().contains(to)) { + throw new UnsupportedOperationException("Unknown namespace: " + to); + } + + for (ClassDef classDef : tinyTree.getClasses()) { + classNames.put(classDef.getName(from), classDef.getName(to)); + + for (FieldDef fieldDef : classDef.getFields()) { + EntryTriple fromEntry = new EntryTriple(classDef.getName(from), fieldDef.getName(from), fieldDef.getDescriptor(from)); + EntryTriple toEntry = new EntryTriple(classDef.getName(to), fieldDef.getName(to), fieldDef.getDescriptor(to)); + fieldNames.put(fromEntry, toEntry); + } + + for (MethodDef methodDef : classDef.getMethods()) { + EntryTriple fromEntry = new EntryTriple(classDef.getName(from), methodDef.getName(from), methodDef.getDescriptor(from)); + EntryTriple toEntry = new EntryTriple(classDef.getName(to), methodDef.getName(to), methodDef.getDescriptor(to)); + methodNames.put(fromEntry, toEntry); + } + } + } + + public AccessWidener remap() { + //Dont remap if we dont need to + if (input.namespace.equals(to)) { + return input; + } + + AccessWidener remapped = new AccessWidener(); + remapped.namespace = to; + + for (Map.Entry entry : input.classAccess.entrySet()) { + remapped.classAccess.put(findMapping(classNames, entry.getKey()), entry.getValue()); + } + + for (Map.Entry entry : input.methodAccess.entrySet()) { + remapped.addOrMerge(remapped.methodAccess, findMapping(methodNames, entry.getKey()), entry.getValue()); + } + + for (Map.Entry entry : input.fieldAccess.entrySet()) { + remapped.addOrMerge(remapped.fieldAccess, findMapping(fieldNames, entry.getKey()), entry.getValue()); + } + + return remapped; + } + + private static V findMapping(Map map, K key) { + V value = map.get(key); + + if (value == null) { + throw new RuntimeException("Failed to find mapping for " + key.toString()); + } + + return value; + } +}