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; + } +}