From 24a83e4ae98af3c1f897dbc77a2c450a4421e1d6 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Thu, 20 Jun 2024 14:32:05 +0100 Subject: [PATCH] Migrate name proposal code to filament --- build.gradle | 12 +- filament/gradle.properties | 2 +- .../filament/nameproposal/Constants.java | 23 +++ .../nameproposal/FieldNameFinder.java | 164 +++++++++++++++++ .../filament/nameproposal/MappingEntry.java | 20 +++ .../nameproposal/MappingNameCompleter.java | 166 ++++++++++++++++++ .../filament/nameproposal/NameFinder.java | 99 +++++++++++ .../nameproposal/NameFinderVisitor.java | 74 ++++++++ .../RecordComponentNameFinder.java | 134 ++++++++++++++ .../enigma/EnigmaNameProposalService.java | 72 ++++++++ .../IntermediaryObfuscationTestService.java | 75 ++++++++ .../NameProposalServiceEnigmaPlugin.java | 36 ++++ .../services/cuchaz.enigma.api.EnigmaPlugin | 1 + gradle.properties | 1 - 14 files changed, 873 insertions(+), 6 deletions(-) create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/Constants.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/FieldNameFinder.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/MappingEntry.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/MappingNameCompleter.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinder.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinderVisitor.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/RecordComponentNameFinder.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/EnigmaNameProposalService.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/IntermediaryObfuscationTestService.java create mode 100644 filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/NameProposalServiceEnigmaPlugin.java create mode 100644 filament/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin diff --git a/build.gradle b/build.gradle index 6b2a762c86..589ef8902a 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,6 @@ buildscript { } dependencies { classpath "cuchaz:enigma-cli:${project.enigma_version}" - classpath "net.fabricmc:name-proposal:${project.name_proposal_version}" } } @@ -71,7 +70,6 @@ def unpickMetaFile = file("unpick-definitions/unpick.json") dependencies { enigmaRuntime "cuchaz:enigma-swing:${project.enigma_version}" - enigmaRuntime "net.fabricmc:name-proposal:${project.name_proposal_version}" enigmaRuntime "net.fabricmc:cfr:${project.cfr_version}" javadocClasspath "net.fabricmc:fabric-loader:${project.fabric_loader_version}" javadocClasspath "org.jetbrains:annotations:${project.jetbrains_annotations_version}" @@ -106,8 +104,8 @@ import net.fabricmc.filament.task.mappingio.CompleteMappingsTask import net.fabricmc.filament.task.mappingio.ConvertMappingsTask import net.fabricmc.filament.task.mappingio.FormatMappingsTask import net.fabricmc.filament.task.mappingio.MergeMappingsTask +import net.fabricmc.filament.nameproposal.MappingNameCompleter import net.fabricmc.mappingio.format.MappingFormat -import net.fabricmc.nameproposal.MappingNameCompleter import org.gradle.work.DisableCachingByDefault import com.diffplug.spotless.LineEnding import groovy.xml.XmlSlurper @@ -624,7 +622,13 @@ abstract class EnigmaTask extends JavaExec { abstract Property getMappings() EnigmaTask() { - classpath = project.configurations.enigmaRuntime + def filamentCodeSource = MappingPoetTask.class.getProtectionDomain().getCodeSource() + def filamentJarFile = new File(filamentCodeSource.getLocation().getFile()) + def runtimeClasspath = project.files() + runtimeClasspath.from(project.configurations.enigmaRuntime) + runtimeClasspath.from(filamentJarFile) + + classpath = runtimeClasspath mainClass.set('cuchaz.enigma.gui.Main') jvmArgs "-Xmx2048m" } diff --git a/filament/gradle.properties b/filament/gradle.properties index 0a148a20aa..cea0d42895 100644 --- a/filament/gradle.properties +++ b/filament/gradle.properties @@ -1 +1 @@ -filament_version=0.8.0 \ No newline at end of file +filament_version=0.9.0 \ No newline at end of file diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/Constants.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/Constants.java new file mode 100644 index 0000000000..f78d960dad --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/Constants.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +import org.objectweb.asm.Opcodes; + +public class Constants { + public static int ASM_VERSION = Opcodes.ASM9; +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/FieldNameFinder.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/FieldNameFinder.java new file mode 100644 index 0000000000..3b1e40ad06 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/FieldNameFinder.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016, 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; + +public class FieldNameFinder { + public Map findNames(Iterable classes) throws Exception { + Map> methods = new HashMap<>(); + Map> enumFields = new HashMap<>(); + + for (byte[] data : classes) { + ClassReader reader = new ClassReader(data); + NameFinderVisitor vClass = new NameFinderVisitor(Constants.ASM_VERSION, enumFields, methods); + reader.accept(vClass, ClassReader.SKIP_FRAMES); + } + + return findNames(enumFields, methods); + } + + public Map findNames(Map> allEnumFields, Map> classes) { + Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); + Map fieldNames = new HashMap<>(); + Map> fieldNamesUsed = new HashMap<>(); + Map> fieldNamesDuplicate = new HashMap<>(); + + for (Map.Entry> entry : classes.entrySet()) { + String owner = entry.getKey(); + Set enumFields = allEnumFields.getOrDefault(owner, Collections.emptySet()); + + for (MethodNode mn : entry.getValue()) { + Frame[] frames; + + try { + frames = analyzer.analyze(owner, mn); + } catch (AnalyzerException e) { + throw new RuntimeException(e); + } + + InsnList instrs = mn.instructions; + + for (int i = 1; i < instrs.size(); i++) { + AbstractInsnNode instr1 = instrs.get(i - 1); + AbstractInsnNode instr2 = instrs.get(i); + String s = null; + + if (instr2.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) instr2).owner.equals(owner) + && (instr1 instanceof MethodInsnNode && ((MethodInsnNode) instr1).owner.equals(owner) || enumFields.contains(((FieldInsnNode) instr2).desc + ((FieldInsnNode) instr2).name)) + && (instr1.getOpcode() == Opcodes.INVOKESTATIC || (instr1.getOpcode() == Opcodes.INVOKESPECIAL && "".equals(((MethodInsnNode) instr1).name)))) { + for (int j = 0; j < frames[i - 1].getStackSize(); j++) { + SourceValue sv = frames[i - 1].getStack(j); + + for (AbstractInsnNode ci : sv.insns) { + if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) { + //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) { + if (s == null) { + s = (String) (((LdcInsnNode) ci).cst); + // stringsFound++; + } + } + } + } + } + + if (s != null) { + if (s.contains(":")) { + s = s.substring(s.indexOf(':') + 1); + } + + if (s.contains("/")) { + int separator = s.indexOf('/'); + String sFirst = s.substring(0, separator); + String sLast; + + if (s.contains(".") && s.indexOf('.') > separator) { + sLast = s.substring(separator + 1, s.indexOf('.')); + } else { + sLast = s.substring(separator + 1); + } + + if (sFirst.endsWith("s")) { + sFirst = sFirst.substring(0, sFirst.length() - 1); + } + + s = sLast + "_" + sFirst; + } + + String oldS = s; + boolean hasAlpha = false; + + for (int j = 0; j < s.length(); j++) { + char c = s.charAt(j); + + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + hasAlpha = true; + } + + if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && !(c == '_')) { + s = s.substring(0, j) + "_" + s.substring(j + 1); + } else if (j > 0 && Character.isUpperCase(s.charAt(j)) && Character.isLowerCase(s.charAt(j - 1))) { + s = s.substring(0, j) + "_" + s.substring(j, j + 1).toLowerCase(Locale.ROOT) + s.substring(j + 1); + } + } + + if (hasAlpha) { + s = s.toUpperCase(Locale.ROOT); + + Set usedNames = fieldNamesUsed.computeIfAbsent(((FieldInsnNode) instr2).owner, (a) -> new HashSet<>()); + Set usedNamesDuplicate = fieldNamesDuplicate.computeIfAbsent(((FieldInsnNode) instr2).owner, (a) -> new HashSet<>()); + + if (!usedNamesDuplicate.contains(s)) { + if (!usedNames.add(s)) { + System.out.println("Warning: Duplicate key: " + s + " (" + oldS + ")!"); + usedNamesDuplicate.add(s); + usedNames.remove(s); + } + } + + if (usedNames.contains(s)) { + fieldNames.put(new MappingEntry(((FieldInsnNode) instr2).owner, ((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc), s); + } + } + } + } + } + } + + return fieldNames; + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/MappingEntry.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/MappingEntry.java new file mode 100644 index 0000000000..d5bc835876 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/MappingEntry.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +public record MappingEntry(String owner, String name, String desc) { +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/MappingNameCompleter.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/MappingNameCompleter.java new file mode 100644 index 0000000000..ed96915b96 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/MappingNameCompleter.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.regex.Pattern; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; + +import net.fabricmc.mappingio.MappedElementKind; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.MappingWriter; +import net.fabricmc.mappingio.format.MappingFormat; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +public class MappingNameCompleter { + // + public static void main(String[] args) throws IOException { + completeNames(Paths.get(args[0]), Paths.get(args[1]), Paths.get(args[2]), Paths.get(args[3])); + } + + public static void completeNames(Path intermediaryJar, Path inputYarnMappings, Path inputIntermediaryMappings, Path outputYarnMappings) throws IOException { + NameFinder nameFinder = new NameFinder(); + + acceptJar(nameFinder, intermediaryJar); + + // We need the full intermediary mappings on their own to lookup the record component's root fields/methods. + nameFinder.acceptIntermediaryMappings(readMappings(inputIntermediaryMappings)); + + Map fieldNames = nameFinder.getFieldNames(); + Map methodNames = nameFinder.getMethodNames(); + Map recordNames = nameFinder.getRecordNames(); + + System.out.printf("Found %d field names%n", fieldNames.size()); + System.out.printf("Found %d method names%n", methodNames.size()); + System.out.printf("Found %d record names%n", recordNames.size()); + + final MemoryMappingTree yarn = readMappings(inputYarnMappings); + final int yarnIntermediaryNs = yarn.getNamespaceId("intermediary"); + final int yarnNamedNs = yarn.getNamespaceId("named"); + + for (Map.Entry entry : fieldNames.entrySet()) { + MappingEntry mappingEntry = entry.getKey(); + + // Ensure there is a class mapping for this + yarn.visitClass(mappingEntry.owner()); + + MemoryMappingTree.ClassMapping classMapping = yarn.getClass(mappingEntry.owner(), yarnIntermediaryNs); + + yarn.visitField(mappingEntry.name(), mappingEntry.desc()); + MappingTree.FieldMapping fieldMapping = Objects.requireNonNull(classMapping.getField(mappingEntry.name(), mappingEntry.desc(), yarnIntermediaryNs), "Could not find field"); + String yarnFieldName = fieldMapping.getName(yarnNamedNs); + + if (yarnFieldName == null || yarnFieldName.startsWith("field_") || yarnFieldName.startsWith("comp_")) { + // Set a new dst name if it doesn't have one, or matches intermediary + yarn.visitDstName(MappedElementKind.FIELD, yarnNamedNs, entry.getValue()); + } + } + + for (Map.Entry entry : methodNames.entrySet()) { + MappingEntry mappingEntry = entry.getKey(); + + // Ensure there is a class mapping for this + yarn.visitClass(mappingEntry.owner()); + + MemoryMappingTree.ClassMapping classMapping = yarn.getClass(mappingEntry.owner(), yarnIntermediaryNs); + + yarn.visitMethod(mappingEntry.name(), mappingEntry.desc()); + MappingTree.MethodMapping methodMapping = Objects.requireNonNull(classMapping.getMethod(mappingEntry.name(), mappingEntry.desc(), yarnIntermediaryNs), "Could not find method"); + String yarnFieldName = methodMapping.getName(yarnNamedNs); + + if (yarnFieldName == null || yarnFieldName.startsWith("method_") || yarnFieldName.startsWith("comp_")) { + // Set a new dst name if it doesn't have one, or matches intermediary + yarn.visitDstName(MappedElementKind.METHOD, yarnNamedNs, entry.getValue()); + } + } + + inheritMappedNamesOfEnclosingClasses(yarn); + + try (MappingWriter mappingWriter = MappingWriter.create(outputYarnMappings, MappingFormat.TINY_2_FILE)) { + yarn.accept(mappingWriter); + } + } + + private static void acceptJar(NameFinder nameFinder, Path jar) throws IOException { + try (JarInputStream jarInputStream = new JarInputStream(Files.newInputStream(jar))) { + JarEntry entry; + + while ((entry = jarInputStream.getNextJarEntry()) != null) { + if (!entry.getName().endsWith(".class")) { + continue; + } + + ClassReader reader = new ClassReader(jarInputStream); + ClassNode classNode = new ClassNode(); + reader.accept(classNode, 0); + + nameFinder.accept(classNode); + } + } + } + + private static MemoryMappingTree readMappings(Path path) throws IOException { + MemoryMappingTree mappingTree = new MemoryMappingTree(); + MappingReader.read(path, mappingTree); + return mappingTree; + } + + /** + * Based off loom: https://github.com/FabricMC/fabric-loom/commit/98d8f3767253a1a3308542c2e896cbf5f4382033. + */ + private static void inheritMappedNamesOfEnclosingClasses(MemoryMappingTree tree) { + int namedIdx = tree.getNamespaceId("named"); + + // The tree does not have an index by intermediary names by default + tree.setIndexByDstNames(true); + + for (MappingTree.ClassMapping classEntry : tree.getClasses()) { + String intermediaryName = Objects.requireNonNull(classEntry.getSrcName()); + String namedName = classEntry.getDstName(namedIdx); + + // No named name, or intermediary name equals the named - and inner class. + if ((namedName == null || intermediaryName.equals(namedName)) && intermediaryName.contains("$")) { + String[] path = intermediaryName.split(Pattern.quote("$")); + int parts = path.length; + + for (int i = parts - 2; i >= 0; i--) { + String currentPath = String.join("$", Arrays.copyOfRange(path, 0, i + 1)); + + String namedParentClass = tree.mapClassName(currentPath, namedIdx); + + if (!namedParentClass.equals(currentPath)) { + classEntry.setDstName(namedParentClass + + "$" + String.join("$", Arrays.copyOfRange(path, i + 1, path.length)), + namedIdx); + break; + } + } + } + } + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinder.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinder.java new file mode 100644 index 0000000000..79d13c71de --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinder.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016, 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +public class NameFinder { + // comp_x -> name + private final Map recordNames = new HashMap<>(); + private final Map recordFieldNames = new HashMap<>(); + private final Map recordMethodNames = new HashMap<>(); + + private final Map> enumFields = new HashMap<>(); + private final Map> methods = new HashMap<>(); + + public void accept(ClassNode classNode) { + classNode.accept(new NameFinderVisitor(Constants.ASM_VERSION, enumFields, methods)); + + if ("java/lang/Record".equals(classNode.superName)) { + classNode.accept(new RecordComponentNameFinder(Constants.ASM_VERSION, recordNames)); + } + } + + public void acceptIntermediaryMappings(MemoryMappingTree mappingTree) { + if (recordNames.isEmpty()) { + return; + } + + final int intermediaryId = mappingTree.getNamespaceId("intermediary"); + + for (Map.Entry entry : recordNames.entrySet()) { + boolean foundMethod = false; + boolean foundField = false; + + for (MappingTree.ClassMapping classMapping : mappingTree.getClasses()) { + for (MappingTree.FieldMapping fieldMapping : classMapping.getFields()) { + if (fieldMapping.getName(intermediaryId).equals(entry.getKey())) { + MappingEntry fieldEntry = new MappingEntry(classMapping.getName(intermediaryId), fieldMapping.getName(intermediaryId), fieldMapping.getDesc(intermediaryId)); + recordFieldNames.put(fieldEntry, entry.getValue()); + foundField = true; + } + } + + for (MappingTree.MethodMapping methodMapping : classMapping.getMethods()) { + if (methodMapping.getName(intermediaryId).equals(entry.getKey())) { + MappingEntry fieldEntry = new MappingEntry(classMapping.getName(intermediaryId), methodMapping.getName(intermediaryId), methodMapping.getDesc(intermediaryId)); + recordMethodNames.put(fieldEntry, entry.getValue()); + foundMethod = true; + } + } + } + + if (!foundField) { + System.err.println("Failed to find field for " + entry); + } + + if (!foundMethod) { + System.err.println("Failed to find method for " + entry); + } + } + } + + public Map getRecordNames() { + return recordNames; + } + + public Map getFieldNames() { + Map fieldNames = new HashMap<>(new FieldNameFinder().findNames(enumFields, methods)); + fieldNames.putAll(recordFieldNames); + return fieldNames; + } + + public Map getMethodNames() { + return recordMethodNames; + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinderVisitor.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinderVisitor.java new file mode 100644 index 0000000000..2417b5b3b1 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/NameFinderVisitor.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016, 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.MethodNode; + +public class NameFinderVisitor extends ClassVisitor { + private String owner; + private final Map> allEnumFields; + private final Map> allMethods; + + public NameFinderVisitor(int api, Map> allEnumFields, Map> allMethods) { + super(api); + this.allMethods = allMethods; + this.allEnumFields = allEnumFields; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + this.owner = name; + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if ((access & Opcodes.ACC_ENUM) != 0) { + if (!allEnumFields.computeIfAbsent(owner, s -> new HashSet<>()).add(descriptor + name)) { + throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\"!"); + } + } + + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod( + final int access, + final String name, + final String descriptor, + final String signature, + final String[] exceptions) { + if ("".equals(name)) { + MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions); + allMethods.computeIfAbsent(owner, s -> new ArrayList<>()).add(node); + return node; + } else { + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/RecordComponentNameFinder.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/RecordComponentNameFinder.java new file mode 100644 index 0000000000..6305f9968f --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/RecordComponentNameFinder.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016, 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal; + +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +public final class RecordComponentNameFinder extends ClassVisitor { + static final Handle OBJ_MTH_BOOTSTRAP = new Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/runtime/ObjectMethods", + "bootstrap", + """ + (Ljava/lang/invoke/MethodHandles$Lookup;\ + Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;\ + Ljava/lang/Class;Ljava/lang/String;\ + [Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;""", + false + ); + static final Set OBJECT_METHODS = Set.of( + new Component("equals", "(Ljava/lang/Object;)Z"), + new Component("toString", "()Ljava/lang/String;"), + new Component("hashCode", "()I") + ); + // comp_x -> name + private final Map recordNames; + private String name; + + public RecordComponentNameFinder(int api, Map recordNames) { + super(api); + this.recordNames = recordNames; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + super.visit(version, access, name, signature, superName, interfaces); + this.name = name; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + var nameAndType = new Component(name, descriptor); + + if (OBJECT_METHODS.contains(nameAndType)) { + return new ObjectMethodVisitor(api, nameAndType); + } + + return null; + } + + private class ObjectMethodVisitor extends MethodVisitor { + private final String owner; + private final Component id; + + ObjectMethodVisitor(int api, Component id) { + super(api); + this.owner = Objects.requireNonNull(RecordComponentNameFinder.this.name); + this.id = id; + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments); + + if (!bootstrapMethodHandle.equals(OBJ_MTH_BOOTSTRAP) || !id.checkIndy(this.owner, name, descriptor)) { + return; + } + + assert bootstrapMethodArguments[0] instanceof Type; + String recordClassName = ((Type) bootstrapMethodArguments[0]).getInternalName(); + + if (!recordClassName.equals(owner)) { + System.out.println("found mismatching object method bootstrap record class in caller class " + owner + "::" + id); + } + + assert bootstrapMethodArguments[1] instanceof String; + String[] names = ((String) bootstrapMethodArguments[1]).split(";"); + assert names.length == bootstrapMethodArguments.length - 2; + + for (int i = 2; i < bootstrapMethodArguments.length; i++) { + if (bootstrapMethodArguments[i] instanceof Handle handle) { + if (handle.getTag() == Opcodes.H_GETFIELD && handle.getOwner().equals(recordClassName)) { + var argName = names[i - 2]; + put(recordNames, handle.getName(), argName); + } else { + // valid bytecode but we cannot guess + System.out.println("found special constant pool method handle, cannot process: " + owner + "::" + id); + } + } else { + // valid bytecode, may be condy bsm arg, can't process + System.out.println("found special bootstrap method arg (maybe condy), cannot process: " + owner + "::" + id); + } + } + } + + private void put(Map out, String key, String value) { + var old = out.put(key, value); + + if (old == null || old.equals(value)) { + return; + } + + System.out.println("Found conflicting name for component " + key + " in " + owner + "::" + + id + ", replaced " + old + " with " + value); + } + } + + private record Component(String name, String desc) { + boolean checkIndy(String owner, String name, String desc) { + return this.name.equals(name) && ("(L" + owner + ";" + this.desc.substring(1)).equals(desc); + } + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/EnigmaNameProposalService.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/EnigmaNameProposalService.java new file mode 100644 index 0000000000..e92e0b6aa5 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/EnigmaNameProposalService.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal.enigma; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.classprovider.ClassProvider; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import org.objectweb.asm.tree.ClassNode; + +import net.fabricmc.filament.nameproposal.MappingEntry; +import net.fabricmc.filament.nameproposal.NameFinder; + +public class EnigmaNameProposalService implements JarIndexerService, NameProposalService { + private Map recordNames; + Map fieldNames; + + @Override + public void acceptJar(Set classNames, ClassProvider classProvider, JarIndex jarIndex) { + NameFinder nameFinder = new NameFinder(); + + for (String className : classNames) { + ClassNode classNode = classProvider.get(className); + nameFinder.accept(Objects.requireNonNull(classNode, "Failed to get ClassNode for " + className)); + } + + recordNames = nameFinder.getRecordNames(); + fieldNames = nameFinder.getFieldNames(); + } + + @Override + public Optional proposeName(Entry obfEntry, EntryRemapper remapper) { + Objects.requireNonNull(recordNames, "Cannot proposeName before indexing"); + + if (obfEntry instanceof FieldEntry fieldEntry) { + if (fieldEntry.getName().startsWith("comp_")) { + return Optional.ofNullable(recordNames.get(fieldEntry.getName())); + } + + return Optional.ofNullable(fieldNames.get(new MappingEntry(fieldEntry.getContainingClass().getFullName(), fieldEntry.getName(), fieldEntry.getDesc().toString()))); + } else if (obfEntry instanceof MethodEntry methodEntry) { + if (methodEntry.getName().startsWith("comp_")) { + return Optional.ofNullable(recordNames.get(methodEntry.getName())); + } + } + + return Optional.empty(); + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/IntermediaryObfuscationTestService.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/IntermediaryObfuscationTestService.java new file mode 100644 index 0000000000..d88843860c --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/IntermediaryObfuscationTestService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016, 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal.enigma; + +import cuchaz.enigma.api.service.EnigmaServiceContext; +import cuchaz.enigma.api.service.ObfuscationTestService; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; + +public class IntermediaryObfuscationTestService implements ObfuscationTestService { + private final String prefix, classPrefix, classPackagePrefix, fieldPrefix, methodPrefix, componentPrefix; + + public IntermediaryObfuscationTestService(EnigmaServiceContext context) { + this.prefix = context.getArgument("package").orElse("net/minecraft") + "/"; + this.classPrefix = context.getArgument("classPrefix").orElse("class_"); + this.fieldPrefix = context.getArgument("fieldPrefix").orElse("field_"); + this.methodPrefix = context.getArgument("methodPrefix").orElse("method_"); + this.componentPrefix = context.getArgument("componentPrefix").orElse("comp_"); + + this.classPackagePrefix = this.prefix + this.classPrefix; + } + + @Override + public boolean testDeobfuscated(Entry entry) { + if (entry instanceof ClassEntry) { + ClassEntry ce = (ClassEntry) entry; + String[] components = ce.getFullName().split("\\$"); + + // all obfuscated components are, at their outermost, class_ + String lastComponent = components[components.length - 1]; + + if (lastComponent.startsWith(this.classPrefix) || lastComponent.startsWith(this.classPackagePrefix)) { + return false; + } + } else if (entry instanceof FieldEntry) { + if (entry.getName().startsWith(this.fieldPrefix)) { + return false; + } + + if (entry.getName().startsWith(this.componentPrefix)) { + return false; + } + } else if (entry instanceof MethodEntry) { + if (entry.getName().startsWith(this.methodPrefix)) { + return false; + } + + if (entry.getName().startsWith(this.componentPrefix)) { + return false; + } + } else { + // unknown type + return false; + } + + // known type, not obfuscated + return true; + } +} diff --git a/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/NameProposalServiceEnigmaPlugin.java b/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/NameProposalServiceEnigmaPlugin.java new file mode 100644 index 0000000000..3fb1fe7272 --- /dev/null +++ b/filament/src/main/java/net/fabricmc/filament/nameproposal/enigma/NameProposalServiceEnigmaPlugin.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.filament.nameproposal.enigma; + +import cuchaz.enigma.api.EnigmaPlugin; +import cuchaz.enigma.api.EnigmaPluginContext; +import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.api.service.ObfuscationTestService; + +public class NameProposalServiceEnigmaPlugin implements EnigmaPlugin { + private static final String ID_PREFIX = "nameproposal:"; + + @Override + public void init(EnigmaPluginContext ctx) { + ctx.registerService(ID_PREFIX + "intermediary_obfuscation_test", ObfuscationTestService.TYPE, IntermediaryObfuscationTestService::new); + + EnigmaNameProposalService service = new EnigmaNameProposalService(); + ctx.registerService(ID_PREFIX + "jar_indexer", JarIndexerService.TYPE, ctx1 -> service); + ctx.registerService(ID_PREFIX + "name_proposal", NameProposalService.TYPE, ctx1 -> service); + } +} diff --git a/filament/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin b/filament/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin new file mode 100644 index 0000000000..0fb570a0cb --- /dev/null +++ b/filament/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin @@ -0,0 +1 @@ +net.fabricmc.filament.nameproposal.enigma.NameProposalServiceEnigmaPlugin \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 095e2a9321..ba62378f5d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,6 @@ enigma_version=2.5.0 unpick_version=2.3.0 cfr_version=0.2.2 vineflower_version=1.9.3 -name_proposal_version=0.2.0 asm_version=9.6 # Javadoc generation/linking