diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java index 2ab68ed85..5f8c11b83 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/Command.java @@ -6,6 +6,7 @@ import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.EnigmaPlugin; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; @@ -106,7 +107,7 @@ public boolean checkArgumentCount(int length) { public static JarIndex loadJar(Path jar) throws IOException { Logger.info("Reading JAR..."); JarClassProvider classProvider = new JarClassProvider(jar); - JarIndex index = JarIndex.empty(); + JarIndex index = MainJarIndex.empty(); index.indexJar(classProvider.getClassNames(), new CachingClassProvider(classProvider), ProgressListener.createEmpty()); return index; diff --git a/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java b/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java index 947d5ebea..e522c53a7 100644 --- a/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java +++ b/enigma-cli/src/main/java/org/quiltmc/enigma/command/FillClassMappingsCommand.java @@ -112,7 +112,7 @@ private static void recursiveAddMappings(EntryTree mappings, JarIn Logger.debug("Entry {} {} a mapping", entry, hasMapping ? "has" : "doesn't have"); if (!hasMapping && addMapping) { Logger.debug("Adding mapping for {}", entry); - mappings.insert(entry, EntryMapping.DEFAULT); + mappings.insert(entry, EntryMapping.OBFUSCATED); } List> children = index.getChildrenByClass().get(entry); diff --git a/enigma-server/src/main/java/org/quiltmc/enigma/network/packet/s2c/SyncMappingsS2CPacket.java b/enigma-server/src/main/java/org/quiltmc/enigma/network/packet/s2c/SyncMappingsS2CPacket.java index 84707938d..8f458faa9 100644 --- a/enigma-server/src/main/java/org/quiltmc/enigma/network/packet/s2c/SyncMappingsS2CPacket.java +++ b/enigma-server/src/main/java/org/quiltmc/enigma/network/packet/s2c/SyncMappingsS2CPacket.java @@ -55,7 +55,7 @@ public void write(DataOutput output) throws IOException { private static void writeEntryTreeNode(DataOutput output, EntryTreeNode node) throws IOException { PacketHelper.writeEntry(output, node.getEntry(), false); EntryMapping value = node.getValue(); - if (value == null) value = EntryMapping.DEFAULT; + if (value == null) value = EntryMapping.OBFUSCATED; PacketHelper.writeString(output, value.targetName() != null ? value.targetName() : ""); PacketHelper.writeString(output, value.javadoc() != null ? value.javadoc() : ""); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java index 6599d153b..3eca9bdfd 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java @@ -2,7 +2,10 @@ import com.google.common.io.MoreFiles; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.LibrariesJarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; +import org.quiltmc.enigma.impl.analysis.ClassLoaderClassProvider; import org.quiltmc.enigma.api.service.EnigmaService; import org.quiltmc.enigma.api.service.EnigmaServiceContext; import org.quiltmc.enigma.api.service.EnigmaServiceFactory; @@ -37,8 +40,10 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.sql.DriverManager; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -74,23 +79,22 @@ public static Builder builder() { public EnigmaProject openJar(Path path, ClassProvider libraryClassProvider, ProgressListener progress) throws IOException { JarClassProvider jarClassProvider = new JarClassProvider(path); - JarIndex index = JarIndex.empty(); - ClassProvider classProvider = new ObfuscationFixClassProvider(new CachingClassProvider(new CombiningClassProvider(jarClassProvider, libraryClassProvider)), index); - Set scope = jarClassProvider.getClassNames(); + JarIndex index = MainJarIndex.empty(); + JarIndex libIndex = LibrariesJarIndex.empty(); - index.indexJar(scope, classProvider, progress); - - var indexers = this.services.get(JarIndexerService.TYPE); - progress.init(indexers.size(), I18n.translate("progress.jar.custom_indexing")); + ClassLoaderClassProvider jreProvider = new ClassLoaderClassProvider(DriverManager.class.getClassLoader()); + CombiningClassProvider librariesProvider = new CombiningClassProvider(jreProvider, libraryClassProvider); + ClassProvider mainProjectProvider = new ObfuscationFixClassProvider(new CachingClassProvider(jarClassProvider), index); - int i = 1; - for (var service : indexers) { - progress.step(i++, I18n.translateFormatted("progress.jar.custom_indexing.indexer", service.getId())); - service.acceptJar(scope, classProvider, index); - } + Set mainScope = new HashSet<>(mainProjectProvider.getClassNames()); + Set librariesScope = new HashSet<>(librariesProvider.getClassNames()); - progress.step(i, I18n.translate("progress.jar.custom_indexing.finished")); + // main index + this.index(index, mainProjectProvider, mainScope, progress, false); + // lib index + this.index(libIndex, librariesProvider, librariesScope, progress, true); + // name proposal var nameProposalServices = this.getNameProposalServices(); progress.init(nameProposalServices.size(), I18n.translate("progress.jar.name_proposal")); @@ -117,7 +121,25 @@ public EnigmaProject openJar(Path path, ClassProvider libraryClassProvider, Prog MappingsIndex mappingsIndex = MappingsIndex.empty(); mappingsIndex.indexMappings(proposedNames, progress); - return new EnigmaProject(this, path, classProvider, index, mappingsIndex, proposedNames, Utils.zipSha1(path)); + return new EnigmaProject(this, path, mainProjectProvider, index, libIndex, mappingsIndex, proposedNames, Utils.zipSha1(path)); + } + + private void index(JarIndex index, ClassProvider classProvider, Set scope, ProgressListener progress, boolean libraries) { + String progressKey = libraries ? "libs" : "jar"; + index.indexJar(scope, classProvider, progress); + + List indexers = this.services.get(JarIndexerService.TYPE); + progress.init(indexers.size(), I18n.translate("progress." + progressKey + ".custom_indexing")); + + int i = 1; + for (var service : indexers) { + if (!(libraries && !service.shouldIndexLibraries())) { + progress.step(i++, I18n.translateFormatted("progress." + progressKey + ".custom_indexing.indexer", service.getId())); + service.acceptJar(scope, classProvider, index); + } + } + + progress.step(i, I18n.translate("progress." + progressKey + ".custom_indexing.finished")); } public EnigmaProfile getProfile() { @@ -172,6 +194,12 @@ public Optional getReadWriteService(Path path) { return this.parseFileType(path).flatMap(this::getReadWriteService); } + public static void validatePluginId(String id) { + if (id != null && !id.matches("([a-z0-9_]+):([a-z0-9_]+((/[a-z0-9_]+)+)?)")) { + throw new IllegalArgumentException("Invalid plugin id: \"" + id + "\"\n" + "Refer to Javadoc on EnigmaService#getId for how to properly form a service ID."); + } + } + /** * Determines the mapping format of the provided path. Checks all formats according to their {@link FileType} file extensions. * If the path is a directory, it will check the first file in the directory. For directories, defaults to the enigma mappings format. @@ -335,7 +363,7 @@ EnigmaServices buildServices() { } private void validateRegistration(ImmutableListMultimap, EnigmaService> services, EnigmaServiceType serviceType, EnigmaService service) { - this.validatePluginId(service.getId()); + validatePluginId(service.getId()); for (EnigmaService otherService : services.get(serviceType)) { // all services @@ -355,12 +383,6 @@ private void validateRegistration(ImmutableListMultimap, En } } } - - private void validatePluginId(String id) { - if (!id.matches("([a-z0-9_]+):([a-z0-9_]+((/[a-z0-9_]+)+)?)")) { - throw new IllegalArgumentException("Invalid plugin id: \"" + id + "\"\n" + "Refer to Javadoc on EnigmaService#getId for how to properly form a service ID."); - } - } } static { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java index 09a9204d2..4d1384e64 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java @@ -29,7 +29,6 @@ import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.util.I18n; -import org.quiltmc.enigma.util.Pair; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; import org.tinylog.Logger; @@ -41,7 +40,6 @@ import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -54,38 +52,23 @@ import java.util.stream.Stream; public class EnigmaProject { - private static final List> NON_RENAMABLE_METHODS = new ArrayList<>(); - - static { - NON_RENAMABLE_METHODS.add(new Pair<>("hashCode", "()I")); - NON_RENAMABLE_METHODS.add(new Pair<>("clone", "()Ljava/lang/Object;")); - NON_RENAMABLE_METHODS.add(new Pair<>("equals", "(Ljava/lang/Object;)Z")); - NON_RENAMABLE_METHODS.add(new Pair<>("finalize", "()V")); - NON_RENAMABLE_METHODS.add(new Pair<>("getClass", "()Ljava/lang/Class;")); - NON_RENAMABLE_METHODS.add(new Pair<>("notify", "()V")); - NON_RENAMABLE_METHODS.add(new Pair<>("notifyAll", "()V")); - NON_RENAMABLE_METHODS.add(new Pair<>("toString", "()Ljava/lang/String;")); - NON_RENAMABLE_METHODS.add(new Pair<>("wait", "()V")); - NON_RENAMABLE_METHODS.add(new Pair<>("wait", "(J)V")); - NON_RENAMABLE_METHODS.add(new Pair<>("wait", "(JI)V")); - } - private final Enigma enigma; - private final Path jarPath; private final ClassProvider classProvider; private final JarIndex jarIndex; - private MappingsIndex mappingsIndex; + private final JarIndex libIndex; private final byte[] jarChecksum; private EntryRemapper remapper; + private MappingsIndex mappingsIndex; - public EnigmaProject(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, MappingsIndex mappingsIndex, EntryTree proposedNames, byte[] jarChecksum) { + public EnigmaProject(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, MappingsIndex mappingsIndex, EntryTree proposedNames, byte[] jarChecksum) { Preconditions.checkArgument(jarChecksum.length == 20); this.enigma = enigma; this.jarPath = jarPath; this.classProvider = classProvider; this.jarIndex = jarIndex; + this.libIndex = libIndex; this.jarChecksum = jarChecksum; this.mappingsIndex = mappingsIndex; @@ -199,18 +182,17 @@ public boolean isRenamable(Entry obfEntry) { String name = obfMethodEntry.getName(); String sig = obfMethodEntry.getDesc().toString(); - // todo change this to a check if the method is declared in java.lang.Object or java.lang.Record - - for (Pair pair : NON_RENAMABLE_METHODS) { - if (pair.a().equals(name) && pair.b().equals(sig)) { - return false; - } + // methods declared in object and record are not renamable + // note: compareTo ignores parent, we want that + if (this.libIndex.getChildrenByClass().get(new ClassEntry("java/lang/Object")).stream().anyMatch(c -> c instanceof MethodEntry m && m.compareTo(obfMethodEntry) == 0) + || this.libIndex.getChildrenByClass().get(new ClassEntry("java/lang/Record")).stream().anyMatch(c -> c instanceof MethodEntry m && m.compareTo(obfMethodEntry) == 0)) { + return false; } ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(obfMethodEntry.getParent()); if (parent != null && parent.isEnum() && ((name.equals("values") && sig.equals("()[L" + parent.getFullName() + ";")) - || (name.equals("valueOf") && sig.equals("(Ljava/lang/String;)L" + parent.getFullName() + ";")))) { + || isEnumValueOfMethod(parent, obfMethodEntry))) { return false; } } else if (obfEntry instanceof LocalVariableEntry localEntry && !localEntry.isArgument()) { @@ -220,7 +202,7 @@ public boolean isRenamable(Entry obfEntry) { ClassDefEntry parent = this.jarIndex.getIndex(EntryIndex.class).getDefinition(method.getParent()); // if this is the valueOf method of an enum class, the argument shouldn't be able to be renamed. - if (parent.isEnum() && method.getName().equals("valueOf") && method.getDesc().toString().equals("(Ljava/lang/String;)L" + parent.getFullName() + ";")) { + if (isEnumValueOfMethod(parent, method)) { return false; } } else if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { @@ -230,6 +212,10 @@ public boolean isRenamable(Entry obfEntry) { return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); } + private static boolean isEnumValueOfMethod(ClassDefEntry parent, MethodEntry method) { + return parent != null && parent.isEnum() && method.getName().equals("valueOf") && method.getDesc().toString().equals("(Ljava/lang/String;)L" + parent.getFullName() + ";"); + } + public boolean isRenamable(EntryReference, Entry> obfReference) { return obfReference.isNamed() && this.isRenamable(obfReference.getNameableEntry()); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java index 108541098..7fb4e14b3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/EntryIndex.java @@ -21,25 +21,23 @@ public class EntryIndex implements JarIndexer { private final EntryTree tree = new HashEntryTree<>(); - private final Map classes = new HashMap<>(); - private final Map fields = new HashMap<>(); - private final Map methods = new HashMap<>(); - private final Map definitions = new HashMap<>(); + private final Map fieldDefinitions = new HashMap<>(); + private final Map methodDefinitions = new HashMap<>(); + private final Map classDefinitions = new HashMap<>(); @Override public void indexClass(ClassDefEntry classEntry) { - this.definitions.put(classEntry, classEntry); - this.classes.put(classEntry, classEntry.getAccess()); + this.classDefinitions.put(classEntry, classEntry); } @Override public void indexMethod(MethodDefEntry methodEntry) { - this.methods.put(methodEntry, methodEntry.getAccess()); + this.methodDefinitions.put(methodEntry, methodEntry); } @Override public void indexField(FieldDefEntry fieldEntry) { - this.fields.put(fieldEntry, fieldEntry.getAccess()); + this.fieldDefinitions.put(fieldEntry, fieldEntry); } @Override @@ -58,15 +56,15 @@ public void processIndex(JarIndex index) { } public boolean hasClass(ClassEntry entry) { - return this.classes.containsKey(entry); + return this.classDefinitions.containsKey(entry); } public boolean hasMethod(MethodEntry entry) { - return this.methods.containsKey(entry); + return this.methodDefinitions.containsKey(entry); } public boolean hasField(FieldEntry entry) { - return this.fields.containsKey(entry); + return this.fieldDefinitions.containsKey(entry); } public boolean hasEntry(Entry entry) { @@ -91,17 +89,20 @@ public boolean hasEntry(Entry entry) { @Nullable public AccessFlags getMethodAccess(MethodEntry entry) { - return this.methods.get(entry); + var def = this.methodDefinitions.get(entry); + return def == null ? null : def.getAccess(); } @Nullable public AccessFlags getFieldAccess(FieldEntry entry) { - return this.fields.get(entry); + var def = this.fieldDefinitions.get(entry); + return def == null ? null : def.getAccess(); } @Nullable public AccessFlags getClassAccess(ClassEntry entry) { - return this.classes.get(entry); + var def = this.classDefinitions.get(entry); + return def == null ? null : def.getAccess(); } @Nullable @@ -119,20 +120,31 @@ public AccessFlags getEntryAccess(Entry entry) { return null; } + @Nullable public ClassDefEntry getDefinition(ClassEntry entry) { - return this.definitions.get(entry); + return this.classDefinitions.get(entry); + } + + @Nullable + public MethodDefEntry getDefinition(MethodEntry entry) { + return this.methodDefinitions.get(entry); + } + + @Nullable + public FieldDefEntry getDefinition(FieldEntry entry) { + return this.fieldDefinitions.get(entry); } public Collection getClasses() { - return this.classes.keySet(); + return this.classDefinitions.keySet(); } public Collection getMethods() { - return this.methods.keySet(); + return this.methodDefinitions.keySet(); } public Collection getFields() { - return this.fields.keySet(); + return this.fieldDefinitions.keySet(); } /** diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/JarIndex.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/JarIndex.java index 93c0eb025..2a76a3bc7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/JarIndex.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/JarIndex.java @@ -1,86 +1,21 @@ package org.quiltmc.enigma.api.analysis.index.jar; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.HashMultimap; import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimap; -import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.ProgressListener; -import org.quiltmc.enigma.api.analysis.ReferenceTargetType; import org.quiltmc.enigma.api.class_provider.ClassProvider; -import org.quiltmc.enigma.impl.analysis.index.IndexClassVisitor; -import org.quiltmc.enigma.impl.analysis.index.IndexReferenceVisitor; import org.quiltmc.enigma.api.translation.mapping.EntryResolver; -import org.quiltmc.enigma.api.translation.mapping.IndexEntryResolver; -import org.quiltmc.enigma.api.translation.representation.Lambda; -import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldDefEntry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodDefEntry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; -import org.quiltmc.enigma.util.I18n; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; import java.util.Set; -public class JarIndex implements JarIndexer { - private final Set indexedClasses = new HashSet<>(); - private final Map, JarIndexer> indexers = new LinkedHashMap<>(); - private final IndexEntryResolver entryResolver; - - private final Multimap methodImplementations = HashMultimap.create(); - private final ListMultimap> childrenByClass; - - private ProgressListener progress; - - /** - * Creates a new empty index with all provided indexers. - * Indexers will be run in the order they're passed to this constructor. - * @param indexers the indexers to use - */ - public JarIndex(JarIndexer... indexers) { - for (JarIndexer indexer : indexers) { - this.indexers.put(indexer.getClass(), indexer); - } - - this.entryResolver = new IndexEntryResolver(this); - this.childrenByClass = ArrayListMultimap.create(); - } - - /** - * Creates an empty index, configured to use all built-in indexers. - * @return the newly created index - */ - public static JarIndex empty() { - EntryIndex entryIndex = new EntryIndex(); - InheritanceIndex inheritanceIndex = new InheritanceIndex(entryIndex); - ReferenceIndex referenceIndex = new ReferenceIndex(); - BridgeMethodIndex bridgeMethodIndex = new BridgeMethodIndex(entryIndex, inheritanceIndex, referenceIndex); - PackageVisibilityIndex packageVisibilityIndex = new PackageVisibilityIndex(); - EnclosingMethodIndex enclosingMethodIndex = new EnclosingMethodIndex(); - LambdaIndex lambdaIndex = new LambdaIndex(); - return new JarIndex(entryIndex, inheritanceIndex, referenceIndex, bridgeMethodIndex, packageVisibilityIndex, enclosingMethodIndex, lambdaIndex); - } - +public interface JarIndex extends JarIndexer { /** * Gets the index associated with the provided class. * @param clazz the class of the index desired - for example, {@code PackageIndex.class} * @return the index */ - @SuppressWarnings("unchecked") - public T getIndex(Class clazz) { - JarIndexer index = this.indexers.get(clazz); - if (index != null) { - return (T) index; - } else { - throw new IllegalArgumentException("no indexer registered for class " + clazz); - } - } + T getIndex(Class clazz); /** * Runs every configured indexer over the provided jar. @@ -88,160 +23,21 @@ public T getIndex(Class clazz) { * @param classProvider a class provider containing all classes in the jar * @param progress a progress listener to track index completion */ - public void indexJar(Set classNames, ClassProvider classProvider, ProgressListener progress) { - // for use in processIndex - this.progress = progress; - - this.indexedClasses.addAll(classNames); - this.progress.init(4, I18n.translate("progress.jar.indexing")); - - this.progress.step(1, I18n.translate("progress.jar.indexing.entries")); - - for (String className : classNames) { - Objects.requireNonNull(classProvider.get(className)).accept(new IndexClassVisitor(this, Enigma.ASM_VERSION)); - } - - this.progress.step(2, I18n.translate("progress.jar.indexing.references")); - - for (String className : classNames) { - try { - Objects.requireNonNull(classProvider.get(className)).accept(new IndexReferenceVisitor(this, this.getIndex(EntryIndex.class), this.getIndex(InheritanceIndex.class), Enigma.ASM_VERSION)); - } catch (Exception e) { - throw new RuntimeException("Exception while indexing class: " + className, e); - } - } - - this.progress.step(3, I18n.translate("progress.jar.indexing.methods")); - this.getIndex(BridgeMethodIndex.class).findBridgeMethods(); - - this.processIndex(this); - - this.progress = null; - } - - @Override - public void processIndex(JarIndex index) { - this.stepProcessingProgress("progress.jar.indexing.process.jar"); - - this.indexers.forEach((key, indexer) -> { - this.stepProcessingProgress(indexer.getTranslationKey()); - indexer.processIndex(index); - }); - - this.stepProcessingProgress("progress.jar.indexing.process.done"); - } - - private void stepProcessingProgress(String key) { - if (this.progress != null) { - this.progress.step(4, I18n.translateFormatted("progress.jar.indexing.process", I18n.translate(key))); - } - } - - @Override - public void indexClass(ClassDefEntry classEntry) { - if (classEntry.isJre()) { - return; - } - - for (ClassEntry interfaceEntry : classEntry.getInterfaces()) { - if (classEntry.equals(interfaceEntry)) { - throw new IllegalArgumentException("Class cannot be its own interface! " + classEntry); - } - } + void indexJar(Set classNames, ClassProvider classProvider, ProgressListener progress); - this.indexers.forEach((key, indexer) -> indexer.indexClass(classEntry)); - if (classEntry.isInnerClass() && !classEntry.getAccess().isSynthetic()) { - this.childrenByClass.put(classEntry.getParent(), classEntry); - } - } - - @Override - public void indexField(FieldDefEntry fieldEntry) { - if (fieldEntry.getParent().isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexField(fieldEntry)); - if (!fieldEntry.getAccess().isSynthetic()) { - this.childrenByClass.put(fieldEntry.getParent(), fieldEntry); - } - } - - @Override - public void indexMethod(MethodDefEntry methodEntry) { - if (methodEntry.getParent().isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexMethod(methodEntry)); - if (!methodEntry.getAccess().isSynthetic() && !methodEntry.getName().equals("")) { - this.childrenByClass.put(methodEntry.getParent(), methodEntry); - } - - if (!methodEntry.isConstructor()) { - this.methodImplementations.put(methodEntry.getParent().getFullName(), methodEntry); - } - } - - @Override - public void indexClassReference(MethodDefEntry callerEntry, ClassEntry referencedEntry, ReferenceTargetType targetType) { - if (callerEntry.getParent().isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexClassReference(callerEntry, referencedEntry, targetType)); - } - - @Override - public void indexMethodReference(MethodDefEntry callerEntry, MethodEntry referencedEntry, ReferenceTargetType targetType) { - if (callerEntry.getParent().isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexMethodReference(callerEntry, referencedEntry, targetType)); - } - - @Override - public void indexFieldReference(MethodDefEntry callerEntry, FieldEntry referencedEntry, ReferenceTargetType targetType) { - if (callerEntry.getParent().isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexFieldReference(callerEntry, referencedEntry, targetType)); - } - - @Override - public void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTargetType targetType) { - if (callerEntry.getParent().isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexLambda(callerEntry, lambda, targetType)); - } - - @Override - public void indexEnclosingMethod(ClassDefEntry classEntry, EnclosingMethodData enclosingMethodData) { - if (classEntry.isJre()) { - return; - } - - this.indexers.forEach((key, indexer) -> indexer.indexEnclosingMethod(classEntry, enclosingMethodData)); - } - - @Override - public String getTranslationKey() { - return "progress.jar.indexing.jar"; - } - - public EntryResolver getEntryResolver() { - return this.entryResolver; - } + /** + * {@return an entry resolver with this index's contents as context} + */ + EntryResolver getEntryResolver(); - public ListMultimap> getChildrenByClass() { - return this.childrenByClass; - } + /** + * {@return a map of all entries, keyed by their class} + */ + ListMultimap> getChildrenByClass(); - public boolean isIndexed(String internalName) { - return this.indexedClasses.contains(internalName); - } + /** + * @param internalName + * {@return whether this class is included in this index} + */ + boolean isIndexed(String internalName); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/LibrariesJarIndex.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/LibrariesJarIndex.java new file mode 100644 index 000000000..d92e667cb --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/LibrariesJarIndex.java @@ -0,0 +1,25 @@ +package org.quiltmc.enigma.api.analysis.index.jar; + +import org.quiltmc.enigma.impl.analysis.index.AbstractJarIndex; + +public class LibrariesJarIndex extends AbstractJarIndex { + public LibrariesJarIndex(JarIndexer... indexers) { + super(indexers); + } + + /** + * Creates an empty index, configured to use all built-in indexers. + * @return the newly created index + */ + public static JarIndex empty() { + EntryIndex entryIndex = new EntryIndex(); + ReferenceIndex referenceIndex = new ReferenceIndex(); + InheritanceIndex inheritanceIndex = new InheritanceIndex(entryIndex); + return new LibrariesJarIndex(entryIndex, inheritanceIndex, referenceIndex, new BridgeMethodIndex(entryIndex, inheritanceIndex, referenceIndex)); + } + + @Override + public String getTranslationKey() { + return "progress.jar.indexing.libraries"; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/MainJarIndex.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/MainJarIndex.java new file mode 100644 index 000000000..2f6b1e601 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/index/jar/MainJarIndex.java @@ -0,0 +1,29 @@ +package org.quiltmc.enigma.api.analysis.index.jar; + +import org.quiltmc.enigma.impl.analysis.index.AbstractJarIndex; + +public class MainJarIndex extends AbstractJarIndex { + public MainJarIndex(JarIndexer... indexers) { + super(indexers); + } + + /** + * Creates an empty index, configured to use all built-in indexers. + * @return the newly created index + */ + public static JarIndex empty() { + EntryIndex entryIndex = new EntryIndex(); + InheritanceIndex inheritanceIndex = new InheritanceIndex(entryIndex); + ReferenceIndex referenceIndex = new ReferenceIndex(); + BridgeMethodIndex bridgeMethodIndex = new BridgeMethodIndex(entryIndex, inheritanceIndex, referenceIndex); + PackageVisibilityIndex packageVisibilityIndex = new PackageVisibilityIndex(); + EnclosingMethodIndex enclosingMethodIndex = new EnclosingMethodIndex(); + LambdaIndex lambdaIndex = new LambdaIndex(); + return new MainJarIndex(entryIndex, inheritanceIndex, referenceIndex, bridgeMethodIndex, packageVisibilityIndex, enclosingMethodIndex, lambdaIndex); + } + + @Override + public String getTranslationKey() { + return "progress.jar.indexing.jar"; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java index 12c79ce72..9cdf007be 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/JarIndexerService.java @@ -5,6 +5,7 @@ import org.quiltmc.enigma.api.class_provider.ClassProvider; import org.objectweb.asm.ClassVisitor; +import javax.annotation.Nullable; import java.util.Set; /** @@ -15,9 +16,57 @@ public interface JarIndexerService extends EnigmaService { EnigmaServiceType TYPE = EnigmaServiceType.create("jar_indexer"); + /** + * Checks the {@code index_libraries} argument in the context to determine if libraries should be indexed. + * @param context the context for this service + * @return whether libraries should be indexed + */ + static boolean shouldIndexLibraries(@Nullable EnigmaServiceContext context) { + if (context == null) { + return false; + } + + return context.getSingleArgument("index_libraries").map(Boolean::parseBoolean).orElse(false); + } + + /** + * Indexes a collection of classes. + * @param scope a list of class names to be indexed + * @param classProvider a provider to translate class names into {@link ClassNode class nodes} + * @param jarIndex the current jar index + */ void acceptJar(Set scope, ClassProvider classProvider, JarIndex jarIndex); + /** + * Whether this indexer should be run on libraries in addition to the main project being indexed. + * @implNote implementations should use {@link #shouldIndexLibraries(EnigmaServiceContext)} to allow changing this setting via the {@link org.quiltmc.enigma.api.EnigmaProfile profile} + * @return whether this indexer should target libraries + */ + default boolean shouldIndexLibraries() { + return false; + } + + /** + * Creates an indexer service that runs all {@link ClassNode class nodes} through the provided {@link ClassVisitor visitor}. + * @param visitor the visitor to pass classes through + * @param id the service's ID + * @return the indexer service + */ static JarIndexerService fromVisitor(ClassVisitor visitor, String id) { + return fromVisitor(null, visitor, id); + } + + /** + * Creates an indexer service that runs all {@link ClassNode class nodes} through the provided {@link ClassVisitor visitor}. + * Overrides {@link #shouldIndexLibraries()} according to the profile argument described in {@link #shouldIndexLibraries(EnigmaServiceContext)}. + * @param context the profile context for the service + * @param visitor the visitor to pass classes through + * @param id the service's ID + * @return the indexer service + */ + static JarIndexerService fromVisitor(@Nullable EnigmaServiceContext context, ClassVisitor visitor, String id) { + boolean indexLibs = shouldIndexLibraries(context); + return new JarIndexerService() { @Override public void acceptJar(Set scope, ClassProvider classProvider, JarIndex jarIndex) { @@ -33,6 +82,11 @@ public void acceptJar(Set scope, ClassProvider classProvider, JarIndex j public String getId() { return id; } + + @Override + public boolean shouldIndexLibraries() { + return indexLibs; + } }; } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java b/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java index 21d92fd5d..1dbe8eebc 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java @@ -8,6 +8,7 @@ import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.api.translation.representation.ArgumentDescriptor; import org.quiltmc.enigma.api.translation.representation.MethodDescriptor; +import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; import org.quiltmc.enigma.api.translation.representation.entry.FieldDefEntry; @@ -191,6 +192,14 @@ public StatsResult generate(Set includedTypes, ClassEntry classEntry, ClassEntry containingClass = method.getContainingClass(); if (includedTypes.contains(StatType.PARAMETERS) && !this.project.isAnonymousOrLocal(containingClass) && !(((MethodDefEntry) method).getAccess().isSynthetic() && !includeSynthetic)) { + ClassDefEntry def = this.entryIndex.getDefinition(containingClass); + if (def != null && def.isRecord()) { + if (this.isCanonicalConstructor(def, method) + || method.equals(new MethodEntry(containingClass, "equals", new MethodDescriptor("(Ljava/lang/Object;)Z")))) { + continue; + } + } + MethodDescriptor descriptor = method.getDesc(); List argumentDescs = descriptor.getArgumentDescs(); @@ -214,6 +223,43 @@ public StatsResult generate(Set includedTypes, ClassEntry classEntry, return StatsResult.create(mappableCounts, unmappedCounts, false); } + private boolean isCanonicalConstructor(ClassDefEntry record, MethodEntry methodEntry) { + if (!record.isRecord() || !methodEntry.isConstructor()) { + return false; + } + + MethodDescriptor descriptor = methodEntry.getDesc(); + List argumentDescs = descriptor.getArgumentDescs(); + List fields = this.project.getJarIndex().getChildrenByClass().get(record).stream() + .filter(e -> { + if (e instanceof FieldEntry field) { + var access = this.entryIndex.getFieldAccess(field); + return access != null && !access.isSynthetic() && !access.isStatic(); + } + + return false; + }) + .map(e -> (FieldEntry) e) + .toList(); + + // number of parameters must match the number of fields + if (argumentDescs.size() != fields.size()) { + return false; + } + + // match types + for (int i = 0; i < fields.size(); i++) { + FieldEntry field = fields.get(i); + ArgumentDescriptor argument = argumentDescs.get(i); + + if (!field.getDesc().toString().equals(argument.toString())) { + return false; + } + } + + return true; + } + /** * Gets the stats for the provided class. * @param entry the class to get stats for diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryMapping.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryMapping.java index f105f8467..b7b4b5aeb 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryMapping.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryMapping.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.api.translation.mapping; +import org.quiltmc.enigma.api.Enigma; import org.quiltmc.enigma.api.source.TokenType; import javax.annotation.Nullable; @@ -10,7 +11,12 @@ public record EntryMapping( TokenType tokenType, @Nullable String sourcePluginId ) { + /** + * @deprecated to be removed in version 3.0.0. Renamed to {@link EntryMapping#OBFUSCATED}. + */ + @Deprecated(forRemoval = true, since = "2.5.0") public static final EntryMapping DEFAULT = new EntryMapping(null, null, TokenType.OBFUSCATED, null); + public static final EntryMapping OBFUSCATED = new EntryMapping(null, null, TokenType.OBFUSCATED, null); public EntryMapping(@Nullable String targetName) { this(trimWhitespace(targetName), null, targetName == null ? TokenType.OBFUSCATED : TokenType.DEOBFUSCATED, null); @@ -21,7 +27,7 @@ public EntryMapping(@Nullable String targetName, @Nullable String javadoc) { } public EntryMapping { - validateSourcePluginId(sourcePluginId); + Enigma.validatePluginId(sourcePluginId); if (tokenType == null) { throw new RuntimeException("cannot create a mapping without a token type!"); @@ -89,10 +95,4 @@ public static EntryMapping merge(EntryMapping leftMapping, EntryMapping rightMap private static String trimWhitespace(@Nullable String string) { return string == null ? null : string.strip(); } - - private static void validateSourcePluginId(String id) { - if (id != null && !id.matches("([a-z0-9_]+:[a-z0-9_/]+)")) { - throw new IllegalArgumentException("invalid plugin ID: '" + id + "! plugin ID should be all lowercase, only contain letters, numbers, underscores and slashes, and be namespaced separated by a colon."); - } - } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java index 3bb4b6c60..41172c434 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.api.translation.mapping; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.InheritanceIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; @@ -15,9 +14,7 @@ import org.quiltmc.enigma.api.translation.mapping.tree.MergedEntryMappingTree; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; -import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; -import org.quiltmc.enigma.util.validation.Message; import org.quiltmc.enigma.util.validation.ValidationContext; import java.util.ArrayList; @@ -76,14 +73,7 @@ public void putMapping(ValidationContext vc, Entry obfuscatedEntry, @Nonnull this.doPutMapping(vc, obfuscatedEntry, deobfMapping, false); } - // note: just supressing warnings until it's fixed - @SuppressWarnings("all") private void doPutMapping(ValidationContext vc, Entry obfuscatedEntry, @Nonnull EntryMapping deobfMapping, boolean validateOnly) { - // todo this needs to be fixed! - //if (obfuscatedEntry instanceof FieldEntry fieldEntry) { - // mapRecordComponentGetter(vc, fieldEntry.getParent(), fieldEntry, deobfMapping); - //} - EntryMapping oldMapping = this.getMapping(obfuscatedEntry); boolean renaming = !Objects.equals(oldMapping.targetName(), deobfMapping.targetName()); @@ -98,7 +88,7 @@ private void doPutMapping(ValidationContext vc, Entry obfuscatedEntry, @Nonnu if (validateOnly || !vc.canProceed()) return; for (Entry resolvedEntry : resolvedEntries) { - if (deobfMapping.equals(EntryMapping.DEFAULT)) { + if (deobfMapping.equals(EntryMapping.OBFUSCATED)) { this.mappings.insert(resolvedEntry, null); } else { this.mappings.insert(resolvedEntry, deobfMapping); @@ -144,40 +134,6 @@ private Collection> resolveAllRoots(Entry obfuscatedEntry) { return resolution; } - // todo this needs to be fixed for hashed mappings! - // note: just supressing warnings until it's fixed - @SuppressWarnings("all") - private void mapRecordComponentGetter(ValidationContext vc, ClassEntry classEntry, FieldEntry fieldEntry, EntryMapping fieldMapping) { - EntryIndex entryIndex = this.jarIndex.getIndex(EntryIndex.class); - - if (!entryIndex.getDefinition(classEntry).isRecord() || entryIndex.getFieldAccess(fieldEntry).isStatic()) { - return; - } - - // Find all the methods in this record class - List classMethods = entryIndex.getMethods().stream() - .filter(entry -> classEntry.equals(entry.getParent())) - .toList(); - - MethodEntry methodEntry = null; - - for (MethodEntry method : classMethods) { - // Find the matching record component getter via matching the names. TODO: Support when the record field and method names do not match - if (method.getName().equals(fieldEntry.getName()) && method.getDesc().toString().equals("()" + fieldEntry.getDesc())) { - methodEntry = method; - break; - } - } - - if (methodEntry == null && fieldMapping != null) { - vc.raise(Message.UNKNOWN_RECORD_GETTER, fieldMapping.targetName()); - return; - } - - // Also remap the associated method, without the javadoc. - this.doPutMapping(vc, methodEntry, new EntryMapping(fieldMapping.targetName()), false); - } - /** * Runs {@link NameProposalService#getDynamicProposedNames(EntryRemapper, Entry, EntryMapping, EntryMapping)} over the names stored in this remapper, * inserting all mappings generated. @@ -194,7 +150,7 @@ public void insertDynamicallyProposedMappings(@Nullable Entry obfEntry, @Null @Nonnull public EntryMapping getMapping(Entry entry) { EntryMapping entryMapping = this.mappings.get(entry); - return entryMapping == null ? EntryMapping.DEFAULT : entryMapping; + return entryMapping == null ? EntryMapping.OBFUSCATED : entryMapping; } public TranslateResult extendedDeobfuscate(T translatable) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/enigma/EnigmaMappingsWriter.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/enigma/EnigmaMappingsWriter.java index 451db2c53..a9e4f0910 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/enigma/EnigmaMappingsWriter.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/enigma/EnigmaMappingsWriter.java @@ -178,7 +178,7 @@ protected void writeRoot(PrintWriter writer, EntryTree mappings, C EntryMapping classEntryMapping = mappings.get(classEntry); if (classEntryMapping == null) { - classEntryMapping = EntryMapping.DEFAULT; + classEntryMapping = EntryMapping.OBFUSCATED; } writer.println(this.writeClass(classEntry, classEntryMapping).trim()); @@ -209,7 +209,7 @@ protected void writeEntry(PrintWriter writer, EntryTree mappings, EntryMapping mapping = node.getValue(); if (mapping == null) { - mapping = EntryMapping.DEFAULT; + mapping = EntryMapping.OBFUSCATED; } String line = null; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java index 24ace2f4d..71c89b28d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/serde/tinyv2/TinyV2Writer.java @@ -150,7 +150,7 @@ private void writeMethod(PrintWriter writer, EntryTreeNode node) { EntryMapping mapping = node.getValue(); if (mapping == null) { - mapping = EntryMapping.DEFAULT; + mapping = EntryMapping.OBFUSCATED; } if (mapping.targetName() != null) { @@ -172,7 +172,7 @@ private void writeMethod(PrintWriter writer, EntryTreeNode node) { } private void writeField(PrintWriter writer, EntryTreeNode node) { - if (node.getValue() == null || node.getValue().equals(EntryMapping.DEFAULT)) { + if (node.getValue() == null || node.getValue().equals(EntryMapping.OBFUSCATED)) { return; // Shortcut } @@ -185,7 +185,7 @@ private void writeField(PrintWriter writer, EntryTreeNode node) { EntryMapping mapping = node.getValue(); if (mapping == null) { - mapping = EntryMapping.DEFAULT; + mapping = EntryMapping.OBFUSCATED; } if (mapping.targetName() != null) { @@ -198,7 +198,7 @@ private void writeField(PrintWriter writer, EntryTreeNode node) { } private void writeParameter(PrintWriter writer, EntryTreeNode node) { - if (node.getValue() == null || node.getValue().equals(EntryMapping.DEFAULT)) { + if (node.getValue() == null || node.getValue().equals(EntryMapping.OBFUSCATED)) { return; // Shortcut } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java index bd617944f..875f73c1f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/tree/MergedEntryMappingTree.java @@ -42,7 +42,7 @@ public EntryMapping remove(Entry entry) { @Override public EntryMapping get(Entry entry) { EntryMapping main = this.mainTree.get(entry); - if (main == null || (main.equals(EntryMapping.DEFAULT) && this.secondaryTree.contains(entry))) { + if (main == null || (main.equals(EntryMapping.OBFUSCATED) && this.secondaryTree.contains(entry))) { return this.secondaryTree.get(entry); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java index ee6ad06cd..0858e17e0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java @@ -40,7 +40,7 @@ private EntryMapping resolveMapping(EntryResolver resolver, EntryMap clazz = this.loader.loadClass(name); + String className = clazz.getName(); + int i = className.lastIndexOf('.'); + String resourceName = className.substring(i != -1 ? i + 1 : 0) + ".class"; + + try (var resource = clazz.getResourceAsStream(resourceName)) { + return AsmUtil.bytesToNode(resource.readAllBytes()); + } catch (IOException ignored) { + // ignored + } + } catch (ClassNotFoundException ignored) { + // ignored + } + + return null; + } + + @Override + public Collection getClassNames() { + return List.of("java.lang.Object", "java.lang.Record"); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/analysis/index/AbstractJarIndex.java b/enigma/src/main/java/org/quiltmc/enigma/impl/analysis/index/AbstractJarIndex.java new file mode 100644 index 000000000..31145fc2d --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/analysis/index/AbstractJarIndex.java @@ -0,0 +1,202 @@ +package org.quiltmc.enigma.impl.analysis.index; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.ReferenceTargetType; +import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.InheritanceIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndexer; +import org.quiltmc.enigma.api.class_provider.ClassProvider; +import org.quiltmc.enigma.api.translation.mapping.EntryResolver; +import org.quiltmc.enigma.api.translation.mapping.IndexEntryResolver; +import org.quiltmc.enigma.api.translation.representation.Lambda; +import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.api.translation.representation.entry.ParentedEntry; +import org.quiltmc.enigma.util.I18n; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public abstract class AbstractJarIndex implements JarIndex { + private final Set indexedClasses = new HashSet<>(); + private final Map, JarIndexer> indexers = new LinkedHashMap<>(); + private final IndexEntryResolver entryResolver; + + private final Multimap methodImplementations = HashMultimap.create(); + private final ListMultimap> childrenByClass; + + private ProgressListener progress; + + /** + * Creates a new empty index with all provided indexers. + * Indexers will be run in the order they're passed to this constructor. + * @param indexers the indexers to use + */ + public AbstractJarIndex(JarIndexer... indexers) { + for (JarIndexer indexer : indexers) { + this.indexers.put(indexer.getClass(), indexer); + } + + this.entryResolver = new IndexEntryResolver(this); + this.childrenByClass = ArrayListMultimap.create(); + } + + /** + * Gets the index associated with the provided class. + * @param clazz the class of the index desired - for example, {@code PackageIndex.class} + * @return the index + */ + @SuppressWarnings("unchecked") + public T getIndex(Class clazz) { + JarIndexer index = this.indexers.get(clazz); + if (index != null) { + return (T) index; + } else { + throw new IllegalArgumentException("no indexer registered for class " + clazz); + } + } + + /** + * Runs every configured indexer over the provided jar. + * @param classNames the obfuscated names of each class in the jar + * @param classProvider a class provider containing all classes in the jar + * @param progress a progress listener to track index completion + */ + public void indexJar(Set classNames, ClassProvider classProvider, ProgressListener progress) { + // for use in processIndex + this.progress = progress; + + this.indexedClasses.addAll(classNames); + this.progress.init(4, I18n.translate("progress.jar.indexing")); + + this.progress.step(1, I18n.translate("progress.jar.indexing.entries")); + + for (String className : classNames) { + Objects.requireNonNull(classProvider.get(className)).accept(new IndexClassVisitor(this, Enigma.ASM_VERSION)); + } + + this.progress.step(2, I18n.translate("progress.jar.indexing.references")); + + for (String className : classNames) { + try { + Objects.requireNonNull(classProvider.get(className)).accept(new IndexReferenceVisitor(this, this.getIndex(EntryIndex.class), this.getIndex(InheritanceIndex.class), Enigma.ASM_VERSION)); + } catch (Exception e) { + throw new RuntimeException("Exception while indexing class: " + className, e); + } + } + + this.progress.step(3, I18n.translate("progress.jar.indexing.methods")); + this.getIndex(BridgeMethodIndex.class).findBridgeMethods(); + + this.processIndex(this); + + this.progress = null; + } + + @Override + public void processIndex(JarIndex index) { + this.stepProcessingProgress("progress.jar.indexing.process.jar"); + + this.indexers.forEach((key, indexer) -> { + this.stepProcessingProgress(indexer.getTranslationKey()); + indexer.processIndex(index); + }); + + this.stepProcessingProgress("progress.jar.indexing.process.done"); + } + + private void stepProcessingProgress(String key) { + if (this.progress != null) { + this.progress.step(4, I18n.translateFormatted("progress.jar.indexing.process", I18n.translate(key))); + } + } + + @Override + public void indexClass(ClassDefEntry classEntry) { + for (ClassEntry interfaceEntry : classEntry.getInterfaces()) { + if (classEntry.equals(interfaceEntry)) { + throw new IllegalArgumentException("Class cannot be its own interface! " + classEntry); + } + } + + this.indexers.forEach((key, indexer) -> indexer.indexClass(classEntry)); + if (classEntry.isInnerClass() && !classEntry.getAccess().isSynthetic()) { + this.childrenByClass.put(classEntry.getParent(), classEntry); + } + } + + @Override + public void indexField(FieldDefEntry fieldEntry) { + this.indexers.forEach((key, indexer) -> indexer.indexField(fieldEntry)); + if (!fieldEntry.getAccess().isSynthetic()) { + this.childrenByClass.put(fieldEntry.getParent(), fieldEntry); + } + } + + @Override + public void indexMethod(MethodDefEntry methodEntry) { + this.indexers.forEach((key, indexer) -> indexer.indexMethod(methodEntry)); + if (!methodEntry.getAccess().isSynthetic() && !methodEntry.getName().equals("")) { + this.childrenByClass.put(methodEntry.getParent(), methodEntry); + } + + if (!methodEntry.isConstructor()) { + this.methodImplementations.put(methodEntry.getParent().getFullName(), methodEntry); + } + } + + @Override + public void indexClassReference(MethodDefEntry callerEntry, ClassEntry referencedEntry, ReferenceTargetType targetType) { + this.indexers.forEach((key, indexer) -> indexer.indexClassReference(callerEntry, referencedEntry, targetType)); + } + + @Override + public void indexMethodReference(MethodDefEntry callerEntry, MethodEntry referencedEntry, ReferenceTargetType targetType) { + this.indexers.forEach((key, indexer) -> indexer.indexMethodReference(callerEntry, referencedEntry, targetType)); + } + + @Override + public void indexFieldReference(MethodDefEntry callerEntry, FieldEntry referencedEntry, ReferenceTargetType targetType) { + this.indexers.forEach((key, indexer) -> indexer.indexFieldReference(callerEntry, referencedEntry, targetType)); + } + + @Override + public void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTargetType targetType) { + this.indexers.forEach((key, indexer) -> indexer.indexLambda(callerEntry, lambda, targetType)); + } + + @Override + public void indexEnclosingMethod(ClassDefEntry classEntry, EnclosingMethodData enclosingMethodData) { + this.indexers.forEach((key, indexer) -> indexer.indexEnclosingMethod(classEntry, enclosingMethodData)); + } + + @Override + public EntryResolver getEntryResolver() { + return this.entryResolver; + } + + @Override + public ListMultimap> getChildrenByClass() { + return this.childrenByClass; + } + + @Override + public boolean isIndexed(String internalName) { + return this.indexedClasses.contains(internalName); + } +} + diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java index 24c2f9c0d..c957c1a6e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/BuiltinPlugin.java @@ -13,6 +13,8 @@ import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import javax.annotation.Nullable; import java.util.HashMap; @@ -21,6 +23,7 @@ public final class BuiltinPlugin implements EnigmaPlugin { @Override public void init(EnigmaPluginContext ctx) { + registerRecordNamingService(ctx); registerEnumNamingService(ctx); registerSpecializedMethodNamingService(ctx); registerDecompilerServices(ctx); @@ -31,7 +34,7 @@ private static void registerEnumNamingService(EnigmaPluginContext ctx) { final Map, String> names = new HashMap<>(); final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); - ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor, "enigma:enum_initializer_indexer")); + ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(ctx1, visitor, "enigma:enum_initializer_indexer")); ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { @Override @@ -59,6 +62,14 @@ public String getId() { }); } + private static void registerRecordNamingService(EnigmaPluginContext ctx) { + final Map fieldToGetter = new HashMap<>(); + final RecordGetterFindingVisitor visitor = new RecordGetterFindingVisitor(fieldToGetter); + + ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(ctx1, visitor, "enigma:record_component_indexer")); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new RecordComponentProposalService(fieldToGetter)); + } + private static void registerSpecializedMethodNamingService(EnigmaPluginContext ctx) { ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { @Override diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java new file mode 100644 index 000000000..95eb8432e --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordComponentProposalService.java @@ -0,0 +1,89 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public record RecordComponentProposalService(Map fieldToGetter) implements NameProposalService { + @Nullable + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + return null; + } + + @Nullable + @Override + public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { + if (obfEntry instanceof FieldEntry fieldEntry) { + return this.mapRecordComponentGetter(remapper, fieldEntry.getContainingClass(), fieldEntry, newMapping); + } else if (obfEntry == null) { + Map, EntryMapping> mappings = new HashMap<>(); + for (var mapping : remapper.getMappings()) { + if (mapping.getEntry() instanceof FieldEntry fieldEntry) { + var getter = this.mapRecordComponentGetter(remapper, fieldEntry.getContainingClass(), fieldEntry, mapping.getValue()); + if (getter != null) { + mappings.putAll(getter); + } + } + } + + return mappings; + } + + return null; + } + + @Nullable + private Map, EntryMapping> mapRecordComponentGetter(EntryRemapper remapper, ClassEntry parent, FieldEntry obfFieldEntry, EntryMapping mapping) { + EntryIndex entryIndex = remapper.getJarIndex().getIndex(EntryIndex.class); + ClassDefEntry parentDef = entryIndex.getDefinition(parent); + var def = entryIndex.getDefinition(obfFieldEntry); + if ((parentDef != null && !parentDef.isRecord()) || (def != null && def.getAccess().isStatic())) { + return null; + } + + List obfClassMethods = remapper.getJarIndex().getChildrenByClass().get(parentDef).stream() + .filter(e -> e instanceof MethodEntry) + .map(e -> (MethodEntry) e) + .toList(); + + MethodEntry obfMethodEntry = null; + for (MethodEntry method : obfClassMethods) { + if (this.isGetter(obfFieldEntry, method)) { + obfMethodEntry = method; + break; + } + } + + if (obfMethodEntry == null) { + return null; + } + + // remap method to match field + EntryMapping newMapping = mapping.tokenType() == TokenType.OBFUSCATED ? new EntryMapping(null, null, TokenType.OBFUSCATED, null) : this.createMapping(mapping.targetName(), TokenType.DYNAMIC_PROPOSED); + return Map.of(obfMethodEntry, newMapping); + } + + public boolean isGetter(FieldEntry obfFieldEntry, MethodEntry method) { + var getter = this.fieldToGetter.get(obfFieldEntry); + return getter != null && getter.equals(method); + } + + @Override + public String getId() { + return "enigma:record_component_proposer"; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java new file mode 100644 index 000000000..e05bcae68 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/RecordGetterFindingVisitor.java @@ -0,0 +1,116 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.RecordComponentVisitor; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.RecordComponentNode; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.translation.representation.MethodDescriptor; +import org.quiltmc.enigma.api.translation.representation.TypeDescriptor; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +final class RecordGetterFindingVisitor extends ClassVisitor { + private ClassEntry clazz; + private final Map fieldToMethod; + private final Set recordComponents = new HashSet<>(); + private final Set fields = new HashSet<>(); + private final Set methods = new HashSet<>(); + + RecordGetterFindingVisitor(Map fieldToMethod) { + super(Enigma.ASM_VERSION); + this.fieldToMethod = fieldToMethod; + } + + @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.clazz = (access & Opcodes.ACC_RECORD) != 0 ? new ClassEntry(name) : null; + this.recordComponents.clear(); + } + + @Override + public RecordComponentVisitor visitRecordComponent(final String name, final String descriptor, final String signature) { + this.recordComponents.add(new RecordComponentNode(this.api, name, descriptor, signature)); + return super.visitRecordComponent(name, descriptor, signature); + } + + @Override + public FieldVisitor visitField(final int access, final String name, final String descriptor, final String signature, final Object value) { + if (this.clazz != null && ((access & Opcodes.ACC_PRIVATE) != 0) && this.recordComponents.stream().anyMatch(component -> component.name.equals(name))) { + FieldNode node = new FieldNode(this.api, access, name, descriptor, signature, value); + this.fields.add(node); + return node; + } + + 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 (this.clazz != null && ((access & Opcodes.ACC_PUBLIC) != 0)) { + MethodNode node = new MethodNode(this.api, access, name, descriptor, signature, exceptions); + this.methods.add(node); + return node; + } + + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + super.visitEnd(); + try { + if (this.clazz != null) { + this.collectResults(); + } + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void collectResults() { + for (RecordComponentNode component : this.recordComponents) { + FieldNode field = null; + for (FieldNode node : this.fields) { + if (node.name.equals(component.name) && node.desc.equals(component.descriptor)) { + field = node; + break; + } + } + + if (field == null) { + throw new RuntimeException("Field not found for record component: " + component.name); + } + + for (MethodNode method : this.methods) { + InsnList instructions = method.instructions; + + // match bytecode to exact expected bytecode for a getter + // only check important instructions (ignore new frame instructions, etc.) + if (instructions.size() == 6 + && instructions.get(2).getOpcode() == Opcodes.ALOAD + && instructions.get(3) instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETFIELD + && fieldInsn.owner.equals(this.clazz.getName()) + && fieldInsn.desc.equals(field.desc) + && fieldInsn.name.equals(field.name) + && instructions.get(4).getOpcode() >= Opcodes.IRETURN && instructions.get(4).getOpcode() <= Opcodes.ARETURN) { + this.fieldToMethod.put(new FieldEntry(this.clazz, field.name, new TypeDescriptor(field.desc)), new MethodEntry(this.clazz, method.name, new MethodDescriptor(method.desc))); + } + } + } + } +} + diff --git a/enigma/src/main/resources/profile.json b/enigma/src/main/resources/profile.json index 4dda7c1fc..a3ac3edb8 100644 --- a/enigma/src/main/resources/profile.json +++ b/enigma/src/main/resources/profile.json @@ -6,6 +6,9 @@ }, { "id": "enigma:specialized_bridge_method_indexer" + }, + { + "id": "enigma:record_component_indexer" } ], "name_proposal": [ @@ -14,6 +17,9 @@ }, { "id": "enigma:specialized_method_name_proposer" + }, + { + "id": "enigma:record_component_proposer" } ] } diff --git a/enigma/src/test/java/org/quiltmc/enigma/PackageVisibilityIndexTest.java b/enigma/src/test/java/org/quiltmc/enigma/PackageVisibilityIndexTest.java index 05086e288..33a1c1def 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/PackageVisibilityIndexTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/PackageVisibilityIndexTest.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.analysis.index.jar.PackageVisibilityIndex; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.class_provider.JarClassProvider; @@ -25,7 +26,7 @@ public class PackageVisibilityIndexTest { public PackageVisibilityIndexTest() throws Exception { JarClassProvider jcp = new JarClassProvider(JAR); - this.jarIndex = JarIndex.empty(); + this.jarIndex = MainJarIndex.empty(); this.jarIndex.indexJar(jcp.getClassNames(), jcp, ProgressListener.createEmpty()); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestInnerClasses.java b/enigma/src/test/java/org/quiltmc/enigma/TestInnerClasses.java index 4056189b2..14c7ac935 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestInnerClasses.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestInnerClasses.java @@ -3,6 +3,7 @@ import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; import org.quiltmc.enigma.api.source.Decompiler; @@ -32,7 +33,7 @@ public class TestInnerClasses { public TestInnerClasses() throws Exception { JarClassProvider jcp = new JarClassProvider(JAR); CachingClassProvider classProvider = new CachingClassProvider(jcp); - this.index = JarIndex.empty(); + this.index = MainJarIndex.empty(); this.index.indexJar(jcp.getClassNames(), classProvider, ProgressListener.createEmpty()); this.decompiler = Decompilers.CFR.create(classProvider, new SourceSettings(false, false)); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestIsRenamable.java b/enigma/src/test/java/org/quiltmc/enigma/TestIsRenamable.java new file mode 100644 index 000000000..636fde91c --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/TestIsRenamable.java @@ -0,0 +1,45 @@ +package org.quiltmc.enigma; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class TestIsRenamable { + public static final Path OBF = TestUtil.obfJar("translation"); + public static final Path DEOBF = TestUtil.deobfJar("translation"); + private static EnigmaProject obfProject; + + @BeforeAll + public static void beforeClass() throws Exception { + Enigma enigma = Enigma.create(); + + Files.createDirectories(DEOBF.getParent()); + obfProject = enigma.openJar(OBF, new ClasspathClassProvider(), ProgressListener.createEmpty()); + } + + @Test + public void obfEntries() { + var equals = TestEntryFactory.newMethod("a", "equals", "(Ljava/lang/Object;)Z"); + Assertions.assertFalse(obfProject.isRenamable(equals)); + + // does actually exist in the code + var randomMethod = TestEntryFactory.newMethod("a", "a", "()V"); + Assertions.assertTrue(obfProject.isRenamable(randomMethod)); + + var getClass = TestEntryFactory.newMethod("java/lang/Object", "getClass", "()Ljava/lang/Class;"); + Assertions.assertFalse(obfProject.isRenamable(getClass)); + + var toString = TestEntryFactory.newMethod("java/lang/Record", "toString", "()Ljava/lang/String;"); + Assertions.assertFalse(obfProject.isRenamable(toString)); + + var wait = TestEntryFactory.newMethod("b", "wait", "()V"); + Assertions.assertFalse(obfProject.isRenamable(wait)); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java index e5de80399..fbf4a5b5a 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexBridgeMethods.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; @@ -38,7 +39,7 @@ public class TestJarIndexBridgeMethods { public TestJarIndexBridgeMethods() throws Exception { JarClassProvider jcp = new JarClassProvider(JAR); this.classProvider = new CachingClassProvider(jcp); - this.index = JarIndex.empty(); + this.index = MainJarIndex.empty(); this.index.indexJar(jcp.getClassNames(), this.classProvider, ProgressListener.createEmpty()); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexConstructorReferences.java b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexConstructorReferences.java index 47c2ea161..52ca2a129 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexConstructorReferences.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexConstructorReferences.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.analysis.index.jar.ReferenceIndex; import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; @@ -32,7 +33,7 @@ public class TestJarIndexConstructorReferences { public TestJarIndexConstructorReferences() throws Exception { JarClassProvider jcp = new JarClassProvider(JAR); - this.index = JarIndex.empty(); + this.index = MainJarIndex.empty(); this.index.indexJar(jcp.getClassNames(), new CachingClassProvider(jcp), ProgressListener.createEmpty()); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexInheritanceTree.java b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexInheritanceTree.java index b66f9a3cc..63392f953 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexInheritanceTree.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexInheritanceTree.java @@ -5,6 +5,7 @@ import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.InheritanceIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.analysis.index.jar.ReferenceIndex; import org.quiltmc.enigma.api.class_provider.CachingClassProvider; import org.quiltmc.enigma.api.class_provider.JarClassProvider; @@ -39,7 +40,7 @@ public class TestJarIndexInheritanceTree { public TestJarIndexInheritanceTree() throws Exception { JarClassProvider jcp = new JarClassProvider(JAR); - this.index = JarIndex.empty(); + this.index = MainJarIndex.empty(); this.index.indexJar(jcp.getClassNames(), new CachingClassProvider(jcp), ProgressListener.createEmpty()); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexLoneClass.java b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexLoneClass.java index 865673ea6..1eeeefd93 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexLoneClass.java +++ b/enigma/src/test/java/org/quiltmc/enigma/TestJarIndexLoneClass.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.analysis.index.jar.ReferenceIndex; import org.quiltmc.enigma.api.analysis.tree.ClassImplementationsTreeNode; import org.quiltmc.enigma.api.analysis.tree.ClassInheritanceTreeNode; @@ -35,7 +36,7 @@ public class TestJarIndexLoneClass { public TestJarIndexLoneClass() throws Exception { JarClassProvider jcp = new JarClassProvider(JAR); - this.index = JarIndex.empty(); + this.index = MainJarIndex.empty(); this.index.indexJar(jcp.getClassNames(), new CachingClassProvider(jcp), ProgressListener.createEmpty()); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/BasicRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/BasicRecord.java new file mode 100644 index 000000000..289ab329a --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/BasicRecord.java @@ -0,0 +1,4 @@ +package org.quiltmc.enigma.input.records; + +public record BasicRecord(int a) { +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/BigRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/BigRecord.java new file mode 100644 index 000000000..8a99cc8f5 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/BigRecord.java @@ -0,0 +1,4 @@ +package org.quiltmc.enigma.input.records; + +public record BigRecord(double a, int b, long c, String d, float e, char f) { +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/ConstructorRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/ConstructorRecord.java new file mode 100644 index 000000000..e8c06295b --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/ConstructorRecord.java @@ -0,0 +1,13 @@ +package org.quiltmc.enigma.input.records; + +public record ConstructorRecord(String a, String b, int c, double d) { + private static final int gaming = 234; + + public ConstructorRecord(String a, String b, double d, int c) { + this(a, b, c, d); + } + + public ConstructorRecord(String b) { + this("gaming", b, 1, 2.0); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java new file mode 100644 index 000000000..6d3018a2d --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/NameMismatchRecord.java @@ -0,0 +1,13 @@ +package org.quiltmc.enigma.input.records; + +public record NameMismatchRecord(int i) { + public int a() { + return 103; + } + + // obfuscates to b(), mismatching with the record component name + @Override + public int i() { + return this.i; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/Record.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/Record.java new file mode 100644 index 000000000..7928bc876 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/Record.java @@ -0,0 +1,22 @@ +package org.quiltmc.enigma.input.records; + +public record Record(String a, int b, double c) { + // non canonical + public Record(String a) { + this(a, 0, 0.0); + } + + // non canonical + public Record(String a, double c, int b) { + this(a, b, c); + } + + // canonical with abnormal bytecode + public Record(String a, int b, double c) { + this.a = a; + this.b = b; + this.c = c; + + System.out.println("grind"); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/records/Record2.java b/enigma/src/test/java/org/quiltmc/enigma/input/records/Record2.java new file mode 100644 index 000000000..64a7274e2 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/records/Record2.java @@ -0,0 +1,8 @@ +package org.quiltmc.enigma.input.records; + +public record Record2(String a) { + @Override + public boolean equals(Object obj) { + return obj == this; + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java new file mode 100644 index 000000000..a89660c23 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java @@ -0,0 +1,149 @@ +package org.quiltmc.enigma.records; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.TestEntryFactory; +import org.quiltmc.enigma.TestUtil; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaProfile; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Path; + +public class TestRecordComponentProposal { + private static final Path JAR = TestUtil.obfJar("records"); + private static EnigmaProject project; + + @BeforeAll + static void setupEnigma() throws IOException { + Reader r = new StringReader(""" + { + "services": { + "name_proposal": [ + { + "id": "enigma:record_component_proposer" + } + ], + "jar_indexer": [ + { + "id": "enigma:record_component_indexer" + } + ] + } + }"""); + + EnigmaProfile profile = EnigmaProfile.parse(r); + Enigma enigma = Enigma.builder().setProfile(profile).build(); + project = enigma.openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); + } + + @Test + void testSimpleRecordComponentProposal() { + // basic example + ClassEntry aClass = TestEntryFactory.newClass("a"); + FieldEntry aField = TestEntryFactory.newField(aClass, "a", "I"); + MethodEntry aGetter = TestEntryFactory.newMethod(aClass, "a", "()I"); + + Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(project.getRemapper().getMapping(aGetter).tokenType(), TokenType.OBFUSCATED); + + project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); + + var fieldMapping = project.getRemapper().getMapping(aField); + Assertions.assertEquals(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + Assertions.assertEquals("mapped", fieldMapping.targetName()); + + var getterMapping = project.getRemapper().getMapping(aGetter); + Assertions.assertEquals(TokenType.DYNAMIC_PROPOSED, getterMapping.tokenType()); + Assertions.assertEquals("mapped", getterMapping.targetName()); + Assertions.assertEquals("enigma:record_component_proposer", getterMapping.sourcePluginId()); + } + + @Test + void testMismatchRecordComponentProposal() { + // name of getter mismatches with name of field + ClassEntry cClass = TestEntryFactory.newClass("d"); + FieldEntry aField = TestEntryFactory.newField(cClass, "a", "I"); + MethodEntry fakeAGetter = TestEntryFactory.newMethod(cClass, "a", "()I"); + MethodEntry realAGetter = TestEntryFactory.newMethod(cClass, "b", "()I"); + + Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(project.getRemapper().getMapping(fakeAGetter).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(project.getRemapper().getMapping(realAGetter).tokenType(), TokenType.OBFUSCATED); + + project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); + + var fieldMapping = project.getRemapper().getMapping(aField); + Assertions.assertEquals(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + Assertions.assertEquals("mapped", fieldMapping.targetName()); + + // fake getter should NOT be mapped + var fakeGetterMapping = project.getRemapper().getMapping(fakeAGetter); + Assertions.assertEquals(TokenType.OBFUSCATED, fakeGetterMapping.tokenType()); + + // real getter SHOULD be mapped + var realGetterMapping = project.getRemapper().getMapping(realAGetter); + Assertions.assertEquals(TokenType.DYNAMIC_PROPOSED, realGetterMapping.tokenType()); + Assertions.assertEquals("mapped", realGetterMapping.targetName()); + Assertions.assertEquals("enigma:record_component_proposer", realGetterMapping.sourcePluginId()); + } + + @Test + void testRecordComponentMappingRemoval() { + ClassEntry aClass = TestEntryFactory.newClass("a"); + FieldEntry aField = TestEntryFactory.newField(aClass, "a", "I"); + MethodEntry aGetter = TestEntryFactory.newMethod(aClass, "a", "()I"); + + Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(project.getRemapper().getMapping(aGetter).tokenType(), TokenType.OBFUSCATED); + + // put name, make sure getter matches + project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); + + var fieldMapping = project.getRemapper().getMapping(aField); + Assertions.assertEquals(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + var getterMapping = project.getRemapper().getMapping(aGetter); + Assertions.assertEquals(TokenType.DYNAMIC_PROPOSED, getterMapping.tokenType()); + + // if field becomes obf getter should become obf + project.getRemapper().putMapping(TestUtil.newVC(), aField, EntryMapping.OBFUSCATED); + + var newFieldMapping = project.getRemapper().getMapping(aField); + Assertions.assertEquals(TokenType.OBFUSCATED, newFieldMapping.tokenType()); + var newGetterMapping = project.getRemapper().getMapping(aGetter); + Assertions.assertEquals(TokenType.OBFUSCATED, newGetterMapping.tokenType()); + } + + @Test + void testTypedRecordComponentProposal() { + // verify that proposal works on different types, all other tests use int getters + ClassEntry eClass = TestEntryFactory.newClass("e"); + FieldEntry aField = TestEntryFactory.newField(eClass, "a", "Ljava/lang/String;"); + MethodEntry aGetter = TestEntryFactory.newMethod(eClass, "a", "()Ljava/lang/String;"); + + Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(project.getRemapper().getMapping(aGetter).tokenType(), TokenType.OBFUSCATED); + + project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); + + var fieldMapping = project.getRemapper().getMapping(aField); + Assertions.assertEquals(TokenType.DEOBFUSCATED, fieldMapping.tokenType()); + Assertions.assertEquals("mapped", fieldMapping.targetName()); + + var getterMapping = project.getRemapper().getMapping(aGetter); + Assertions.assertEquals(TokenType.DYNAMIC_PROPOSED, getterMapping.tokenType()); + Assertions.assertEquals("mapped", getterMapping.targetName()); + Assertions.assertEquals("enigma:record_component_proposer", getterMapping.sourcePluginId()); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java new file mode 100644 index 000000000..e8e9ec52f --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordStats.java @@ -0,0 +1,82 @@ +package org.quiltmc.enigma.records; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.TestEntryFactory; +import org.quiltmc.enigma.TestUtil; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaProfile; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; +import org.quiltmc.enigma.api.stats.StatType; +import org.quiltmc.enigma.api.stats.StatsGenerator; +import org.quiltmc.enigma.api.stats.StatsResult; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Path; +import java.util.EnumSet; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class TestRecordStats { + private static final Path JAR = TestUtil.obfJar("records"); + private static EnigmaProject project; + + @BeforeAll + static void setupEnigma() throws IOException { + Reader r = new StringReader(""" + { + "services": { + "name_proposal": [ + { + "id": "enigma:record_component_proposer" + } + ], + "jar_indexer": [ + { + "id": "enigma:record_component_indexer" + } + ] + } + }"""); + + EnigmaProfile profile = EnigmaProfile.parse(r); + Enigma enigma = Enigma.builder().setProfile(profile).build(); + project = enigma.openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); + } + + @Test + void testParameters() { + StatsResult stats = new StatsGenerator(project).generate(EnumSet.of(StatType.PARAMETERS), TestEntryFactory.newClass("c"), false); + + // total params in the class are 10 + // equals method is ignored + // canonical constructor is ignored + // remaining parameters should be 5 + assertThat(stats.getMappable(StatType.PARAMETERS), equalTo(5)); + assertThat(stats.getMapped(StatType.PARAMETERS), equalTo(0)); + } + + @Test + void testMethods() { + ClassEntry c = TestEntryFactory.newClass("c"); + StatsResult stats = new StatsGenerator(project).generate(EnumSet.of(StatType.METHODS), c, false); + + // 4 mappable methods: 1 for each field + assertThat(stats.getMappable(StatType.METHODS), equalTo(4)); + assertThat(stats.getMapped(StatType.METHODS), equalTo(0)); + + project.getRemapper().putMapping(TestUtil.newVC(), TestEntryFactory.newField(c, "a", "Ljava/lang/String;"), new EntryMapping("gaming")); + StatsResult stats2 = new StatsGenerator(project).generate(EnumSet.of(StatType.METHODS), c, false); + + // 1 method mapped to match field + assertThat(stats2.getMappable(StatType.METHODS), equalTo(4)); + assertThat(stats2.getMapped(StatType.METHODS), equalTo(1)); + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/IndexEntryResolverTest.java b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/IndexEntryResolverTest.java index ec1b83cb0..8bf94284a 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/IndexEntryResolverTest.java +++ b/enigma/src/test/java/org/quiltmc/enigma/translation/mapping/IndexEntryResolverTest.java @@ -9,6 +9,7 @@ import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.analysis.index.jar.MainJarIndex; import org.quiltmc.enigma.api.class_provider.ClassProvider; import org.quiltmc.enigma.api.translation.mapping.IndexEntryResolver; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; @@ -45,7 +46,7 @@ public Collection getClassNames() { private static IndexEntryResolver resolver; @BeforeAll public static void beforeAll() { - index = JarIndex.empty(); + index = MainJarIndex.empty(); index.indexJar(new HashSet<>(CLASS_PROVIDER.getClassNames()), CLASS_PROVIDER, ProgressListener.createEmpty()); resolver = new IndexEntryResolver(index); } diff --git a/enigma/src/testFixtures/resources/profile.json b/enigma/src/testFixtures/resources/profile.json index cb69ee3ba..ac5b8375a 100644 --- a/enigma/src/testFixtures/resources/profile.json +++ b/enigma/src/testFixtures/resources/profile.json @@ -6,6 +6,9 @@ }, { "id": "enigma:specialized_bridge_method_indexer" + }, + { + "id": "enigma:record_component_indexer" } ], "name_proposal": [ @@ -17,6 +20,9 @@ }, { "id": "test:parameters" + }, + { + "id": "enigma:record_component_proposer" } ] }