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 3347d00b3..2abea465b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java @@ -143,7 +143,7 @@ public EnigmaProject openJar(Path path, ClassProvider libraryClassProvider, Prog MappingsIndex mappingsIndex = MappingsIndex.empty(); mappingsIndex.indexMappings(proposedNames, progress); - return new EnigmaProject(this, path, mainProjectProvider, jarIndex, libIndex, comboIndex, mappingsIndex, proposedNames, Utils.zipSha1(path)); + return EnigmaProject.of(this, path, mainProjectProvider, jarIndex, libIndex, comboIndex, mappingsIndex, proposedNames, Utils.zipSha1(path)); } private Predicate createMainReferencedPredicate(AbstractJarIndex mainIndex, ProjectClassProvider classProvider) { 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 29bd702a2..eb9ac8009 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java @@ -1,84 +1,34 @@ package org.quiltmc.enigma.api; -import com.google.common.base.Functions; -import com.google.common.base.Preconditions; import org.quiltmc.enigma.api.analysis.EntryReference; -import org.quiltmc.enigma.api.analysis.index.jar.EnclosingMethodIndex; -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.mapping.MappingsIndex; -import org.quiltmc.enigma.api.service.ObfuscationTestService; -import org.quiltmc.enigma.api.source.TokenType; -import org.quiltmc.enigma.api.translation.mapping.EntryResolver; -import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; -import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeUtil; -import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; -import org.quiltmc.enigma.impl.bytecode.translator.TranslationClassVisitor; +import org.quiltmc.enigma.impl.EnigmaProjectImpl; import org.quiltmc.enigma.api.class_provider.ClassProvider; -import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; -import org.quiltmc.enigma.api.source.Decompiler; import org.quiltmc.enigma.api.service.DecompilerService; -import org.quiltmc.enigma.api.source.SourceSettings; -import org.quiltmc.enigma.api.translation.Translator; import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; -import org.quiltmc.enigma.impl.translation.mapping.MappingsChecker; -import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; -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.LocalVariableEntry; -import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; -import org.quiltmc.enigma.util.I18n; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.tree.ClassNode; -import org.tinylog.Logger; import javax.annotation.Nullable; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.stream.Collectors; import java.util.stream.Stream; -public class EnigmaProject { - private final Enigma enigma; - private final Path jarPath; - private final ClassProvider classProvider; - private final JarIndex jarIndex; - private final JarIndex libIndex; - private final JarIndex combinedIndex; - private final byte[] jarChecksum; - private final Map libraryMethodOverrideCache = new HashMap<>(); - - private EntryRemapper remapper; - private MappingsIndex mappingsIndex; - - public EnigmaProject(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, JarIndex combinedIndex, 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.combinedIndex = combinedIndex; - this.jarChecksum = jarChecksum; - - this.mappingsIndex = mappingsIndex; - this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, proposedNames, new HashEntryTree<>(), this.enigma.getNameProposalServices()); +/** + * Represents an Enigma project which applies a set of mappings to a source jar. + * + * @implNote This interface is not intended for implementation by api users. An instance can be created using the + * {@link #of} factory method. + */ +public interface EnigmaProject { + // TODO should this be api? + static EnigmaProject of(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, JarIndex combinedIndex, MappingsIndex mappingsIndex, EntryTree proposedNames, byte[] jarChecksum) { + return new EnigmaProjectImpl(enigma, jarPath, classProvider, jarIndex, libIndex, combinedIndex, mappingsIndex, proposedNames, jarChecksum); } /** @@ -87,224 +37,49 @@ public EnigmaProject(Enigma enigma, Path jarPath, ClassProvider classProvider, J * @param mappings the new mappings * @param progress a progress listener for indexing */ - public void setMappings(@Nullable EntryTree mappings, ProgressListener progress) { - // keep bytecode-based proposed names, to avoid unnecessary recalculation - EntryTree jarProposedMappings = this.remapper != null ? this.remapper.getJarProposedMappings() : new HashEntryTree<>(); + void setMappings(@Nullable EntryTree mappings, ProgressListener progress); - this.mappingsIndex = MappingsIndex.empty(); + Enigma getEnigma(); - if (mappings != null) { - EntryTree mergedTree = EntryTreeUtil.merge(jarProposedMappings, mappings); - - this.mappingsIndex.indexMappings(mergedTree, progress); - this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, mappings, this.enigma.getNameProposalServices()); - } else if (!jarProposedMappings.isEmpty()) { - this.mappingsIndex.indexMappings(jarProposedMappings, progress); - this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, new HashEntryTree<>(), this.enigma.getNameProposalServices()); - } else { - this.remapper = EntryRemapper.empty(this.enigma, this.combinedIndex, this.enigma.getNameProposalServices()); - } - - // update dynamically proposed names - this.remapper.insertDynamicallyProposedMappings(null, null, null); - } + Path getJarPath(); - public Enigma getEnigma() { - return this.enigma; - } - - public Path getJarPath() { - return this.jarPath; - } - - public ClassProvider getClassProvider() { - return this.classProvider; - } + ClassProvider getClassProvider(); /** * Gets the index of the main jar of this project; the jar being mapped. */ - public JarIndex getJarIndex() { - return this.jarIndex; - } + JarIndex getJarIndex(); /** * Gets the index of the library jars of this project. */ - public JarIndex getLibIndex() { - return this.libIndex; - } + JarIndex getLibIndex(); /** * Gets the index of the main jar and library jars of this project. */ - public JarIndex getCombinedIndex() { - return this.combinedIndex; - } - - public MappingsIndex getMappingsIndex() { - return this.mappingsIndex; - } - - public byte[] getJarChecksum() { - return this.jarChecksum; - } - - public EntryRemapper getRemapper() { - return this.remapper; - } - - public Collection> dropMappings(ProgressListener progress) { - DeltaTrackingTree mappings = this.remapper.getMappings(); - - Collection> dropped = this.dropMappings(mappings, progress); - for (Entry entry : dropped) { - mappings.trackChange(entry); - } - - return dropped; - } - - private Collection> dropMappings(EntryTree mappings, ProgressListener progress) { - MappingsChecker.Dropper dropper = new MappingsChecker.Dropper(); - - // drop mappings that don't match the jar - MappingsChecker checker = new MappingsChecker(this, this.jarIndex, mappings); - - checker.collectBrokenMappings(progress, dropper); - - Map, String> droppedBrokenMappings = dropper.getPendingDroppedMappings(); - for (Map.Entry, String> mapping : droppedBrokenMappings.entrySet()) { - Logger.warn("Couldn't find {} ({}) in jar. Mapping was dropped.", mapping.getKey(), mapping.getValue()); - } - - dropper.applyPendingDrops(mappings); - checker.collectEmptyMappings(progress, dropper); - - Map, String> droppedEmptyMappings = dropper.getPendingDroppedMappings(); - for (Map.Entry, String> mapping : droppedEmptyMappings.entrySet()) { - Logger.warn("{} ({}) was empty. Mapping was dropped.", mapping.getKey(), mapping.getValue()); - } - - dropper.applyPendingDrops(mappings); - - return dropper.getDroppedMappings().keySet(); - } - - public boolean isNavigable(Entry obfEntry) { - if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { - return false; - } - - return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); - } - - public boolean isRenamable(Entry obfEntry) { - if (obfEntry instanceof MethodEntry obfMethodEntry) { - // constructors are not renamable! - if (obfMethodEntry.isConstructor()) { - return false; - } - - // HACKHACK: Object methods are not obfuscated identifiers - String name = obfMethodEntry.getName(); - String sig = obfMethodEntry.getDesc().toString(); - - // 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() + ";")) - || isEnumValueOfMethod(parent, obfMethodEntry))) { - return false; - } - - if (this.isLibraryMethodOverride(obfMethodEntry)) { - return false; - } - } else if (obfEntry instanceof LocalVariableEntry localEntry && !localEntry.isArgument()) { - return false; - } else if (obfEntry instanceof LocalVariableEntry localEntry && localEntry.isArgument()) { - MethodEntry method = localEntry.getParent(); - 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 (isEnumValueOfMethod(parent, method)) { - return false; - } - } else if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { - return false; - } + JarIndex getCombinedIndex(); - return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); - } - - private boolean isLibraryMethodOverride(MethodEntry methodEntry) { - final Boolean cached = this.libraryMethodOverrideCache.get(methodEntry); - if (cached != null) { - return cached; - } else { - if (this.combinedIndex.getIndex(EntryIndex.class).hasMethod(methodEntry)) { - final EntryResolver combinedResolver = this.combinedIndex.getEntryResolver(); - final Set equivalents = combinedResolver.resolveEquivalentMethods(methodEntry); - final Set roots = equivalents.stream() - .flatMap(equivalent -> combinedResolver.resolveEntry(equivalent, ResolutionStrategy.RESOLVE_ROOT).stream()) - .collect(Collectors.toSet()); + MappingsIndex getMappingsIndex(); - final Set equivalentsAndRoots = Stream - .concat(equivalents.stream(), roots.stream()) - .collect(Collectors.toSet()); + // TODO should this be api? + byte[] getJarChecksum(); - final EntryIndex jarEntryIndex = this.jarIndex.getIndex(EntryIndex.class); - final boolean anyNonJar = equivalentsAndRoots.stream().anyMatch(method -> !jarEntryIndex.hasMethod(method)); + EntryRemapper getRemapper(); - equivalentsAndRoots.forEach(method -> this.libraryMethodOverrideCache.put(method, anyNonJar)); + Collection> dropMappings(ProgressListener progress); - return anyNonJar; - } else { - this.libraryMethodOverrideCache.put(methodEntry, false); + boolean isNavigable(Entry obfEntry); - return false; - } - } - } + boolean isRenamable(Entry 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() + ";"); - } + boolean isRenamable(EntryReference, Entry> obfReference); - public boolean isRenamable(EntryReference, Entry> obfReference) { - return obfReference.isNamed() && this.isRenamable(obfReference.getNameableEntry()); - } + boolean isObfuscated(Entry entry); - public boolean isObfuscated(Entry entry) { - List obfuscationTestServices = this.getEnigma().getServices().get(ObfuscationTestService.TYPE); - if (!obfuscationTestServices.isEmpty()) { - for (ObfuscationTestService service : obfuscationTestServices) { - if (service.testDeobfuscated(entry)) { - return false; - } - } - } + boolean isSynthetic(Entry entry); - EntryMapping mapping = this.remapper.getMapping(entry); - return mapping.tokenType() == TokenType.OBFUSCATED; - } - - public boolean isSynthetic(Entry entry) { - return this.jarIndex.getIndex(EntryIndex.class).hasEntry(entry) && this.jarIndex.getIndex(EntryIndex.class).getEntryAccess(entry).isSynthetic(); - } - - public boolean isAnonymousOrLocal(ClassEntry classEntry) { - EnclosingMethodIndex enclosingMethodIndex = this.jarIndex.getIndex(EnclosingMethodIndex.class); - // Only local and anonymous classes may have the EnclosingMethod attribute - return enclosingMethodIndex.hasEnclosingMethod(classEntry); - } + boolean isAnonymousOrLocal(ClassEntry classEntry); /** * Verifies that the provided {@code parameter} has a valid index for its parent method. @@ -318,187 +93,33 @@ public boolean isAnonymousOrLocal(ClassEntry classEntry) { * @param parameter the parameter to validate * @return whether the index is valid */ - @SuppressWarnings("DataFlowIssue") - public boolean validateParameterIndex(LocalVariableEntry parameter) { - MethodEntry parent = parameter.getParent(); - EntryIndex index = this.jarIndex.getIndex(EntryIndex.class); - - if (index.hasMethod(parent)) { - AtomicInteger maxLocals = new AtomicInteger(-1); - ClassEntry parentClass = parent.getParent(); - - // find max_locals for method, representing the number of parameters it receives (JVMS§4.7.3) - // note: parent class cannot be null, warning suppressed - ClassNode classNode = this.getClassProvider().get(parentClass.getFullName()); - if (classNode != null) { - classNode.methods.stream() - .filter(node -> node.name.equals(parent.getName()) && node.desc.equals(parent.getDesc().toString())) - .findFirst().ifPresent(node -> { - // occasionally it's possible to run into a method that has parameters, yet whose max locals is 0. java is stupid. we ignore those cases - if (!(node.parameters != null && node.parameters.size() > node.maxLocals)) { - maxLocals.set(node.maxLocals); - } - }); - } - - // if maxLocals is -1 it's not found for the method and should be ignored - return index.validateParameterIndex(parameter) && (maxLocals.get() == -1 || parameter.getIndex() <= maxLocals.get() - 1); - } - - return false; - } - - public JarExport exportRemappedJar(ProgressListener progress) { - Collection classEntries = this.jarIndex.getIndex(EntryIndex.class).getClasses(); - ClassProvider fixingClassProvider = new ObfuscationFixClassProvider(this.classProvider, this.jarIndex); - Translator deobfuscator = this.remapper.getDeobfuscator(); - - AtomicInteger count = new AtomicInteger(); - progress.init(classEntries.size(), I18n.translate("progress.classes.deobfuscating")); - - Map compiled = classEntries.parallelStream() - .map(entry -> { - ClassEntry translatedEntry = deobfuscator.translate(entry); - progress.step(count.getAndIncrement(), translatedEntry.toString()); - - ClassNode node = fixingClassProvider.get(entry.getFullName()); - if (node != null) { - ClassNode translatedNode = new ClassNode(); - node.accept(new TranslationClassVisitor(deobfuscator, Enigma.ASM_VERSION, translatedNode)); - return translatedNode; - } - - return null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toMap(n -> n.name, Functions.identity())); - - return new JarExport(this.remapper, compiled); - } - - public static final class JarExport { - private final EntryRemapper mapper; - private final Map compiled; - - JarExport(EntryRemapper mapper, Map compiled) { - this.mapper = mapper; - this.compiled = compiled; - } - - public void write(Path path, ProgressListener progress) throws IOException { - progress.init(this.compiled.size(), I18n.translate("progress.jar.writing")); - - try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(path))) { - AtomicInteger count = new AtomicInteger(); - - for (ClassNode node : this.compiled.values()) { - progress.step(count.getAndIncrement(), node.name); - - String entryName = node.name.replace('.', '/') + ".class"; - - ClassWriter writer = new ClassWriter(0); - node.accept(writer); - - out.putNextEntry(new JarEntry(entryName)); - out.write(writer.toByteArray()); - out.closeEntry(); - } - } - } - - public SourceExport decompile(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { - List decompiled = this.decompileStream(progress, decompilerService, errorStrategy).toList(); - return new SourceExport(decompiled); - } - - public Stream decompileStream(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { - Collection classes = this.compiled.values().stream() - .filter(classNode -> classNode.name.indexOf('$') == -1) - .toList(); + boolean validateParameterIndex(LocalVariableEntry parameter); - progress.init(classes.size(), I18n.translate("progress.classes.decompiling")); + JarExport exportRemappedJar(ProgressListener progress); - //create a common instance outside the loop as mappings shouldn't be changing while this is happening - Decompiler decompiler = decompilerService.create(ClassProvider.fromMap(this.compiled), new SourceSettings(false, false)); + interface JarExport { + void write(Path path, ProgressListener progress) throws IOException; - AtomicInteger count = new AtomicInteger(); + SourceExport decompile( + ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy + ); - return classes.parallelStream() - .map(translatedNode -> { - progress.step(count.getAndIncrement(), translatedNode.name); - - String source = null; - try { - source = this.decompileClass(translatedNode, decompiler); - } catch (Exception e) { - switch (errorStrategy) { - case PROPAGATE: throw e; - case IGNORE: break; - case TRACE_AS_SOURCE: { - StringWriter writer = new StringWriter(); - e.printStackTrace(new PrintWriter(writer)); - source = writer.toString(); - break; - } - } - } - - if (source == null) { - return null; - } - - return new ClassSource(translatedNode.name, source); - }) - .filter(Objects::nonNull); - } - - private String decompileClass(ClassNode translatedNode, Decompiler decompiler) { - return decompiler.getSource(translatedNode.name, this.mapper).asString(); - } + Stream decompileStream( + ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy + ); } - public static final class SourceExport { - public final Collection decompiled; - - SourceExport(Collection decompiled) { - this.decompiled = decompiled; - } - - public void write(Path path, ProgressListener progress) throws IOException { - progress.init(this.decompiled.size(), I18n.translate("progress.sources.writing")); - - int count = 0; - for (ClassSource source : this.decompiled) { - progress.step(count++, source.name); - - Path sourcePath = source.resolvePath(path); - source.writeTo(sourcePath); - } - } + interface SourceExport { + void write(Path path, ProgressListener progress) throws IOException; } - public static class ClassSource { - public final String name; - public final String source; - - ClassSource(String name, String source) { - this.name = name; - this.source = source; - } - - public void writeTo(Path path) throws IOException { - Files.createDirectories(path.getParent()); - try (BufferedWriter writer = Files.newBufferedWriter(path)) { - writer.write(this.source); - } - } + interface ClassSource { + void writeTo(Path path) throws IOException; - public Path resolvePath(Path root) { - return root.resolve(this.name.replace('.', '/') + ".java"); - } + Path resolvePath(Path root); } - public enum DecompileErrorStrategy { + enum DecompileErrorStrategy { PROPAGATE, TRACE_AS_SOURCE, IGNORE diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java index 3a8b6f40d..59eed99f7 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/analysis/tree/StructureTreeNode.java @@ -12,6 +12,7 @@ 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.impl.EnigmaProjectImpl; import javax.swing.tree.DefaultMutableTreeNode; import java.util.ArrayList; @@ -38,17 +39,21 @@ public ParentedEntry getEntry() { } public void load(EnigmaProject project, StructureTreeOptions options) { + this.loadImpl((EnigmaProjectImpl) project, options); + } + + private void loadImpl(EnigmaProjectImpl project, StructureTreeOptions options) { Stream> children = project.getJarIndex().getChildrenByClass().get(this.parentEntry).stream(); children = switch (options.obfuscationVisibility()) { case ALL -> children; case OBFUSCATED -> children // remove deobfuscated members if only obfuscated, unless it's an inner class - .filter(e -> (e instanceof ClassEntry) || (project.isObfuscated(e) && project.isRenamable(e))) + .filter(e -> (e instanceof ClassEntry) || (project.isObfuscated(e) && project.isInternallyRenamable(e))) // keep constructor methods if the class is obfuscated .filter(e -> !(e instanceof MethodEntry m && m.isConstructor()) || project.isObfuscated(e.getParent())); case DEOBFUSCATED -> children.filter(e -> (e instanceof ClassEntry) - || (!project.isObfuscated(e) && project.isRenamable(e)) + || (!project.isObfuscated(e) && project.isInternallyRenamable(e)) // keep constructor methods if the class is deobfuscated || (e instanceof MethodEntry m && m.isConstructor()) && !project.isObfuscated(e.getParent())); }; diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java index 87879f215..6a747453e 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java @@ -9,6 +9,7 @@ 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.LocalVariableDefEntry; +import org.quiltmc.enigma.impl.EnigmaProjectImpl; import org.quiltmc.enigma.impl.translation.LocalNameGenerator; import javax.annotation.Nullable; @@ -46,12 +47,12 @@ public DecompiledClassSource remapSource(EnigmaProject project, Translator trans SourceRemapper remapper = new SourceRemapper(this.obfuscatedIndex.getSource(), this.obfuscatedIndex.referenceTokens()); TokenStore tokenStore = TokenStore.create(this.obfuscatedIndex); - SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> this.remapToken(tokenStore, project, token, movedToken, translator)); + SourceRemapper.Result remapResult = remapper.remap((token, movedToken) -> this.remapToken(tokenStore, ((EnigmaProjectImpl) project), token, movedToken, translator)); SourceIndex remappedIndex = this.obfuscatedIndex.remapTo(remapResult); return new DecompiledClassSource(this.classEntry, this.obfuscatedIndex, remappedIndex, tokenStore); } - private String remapToken(TokenStore target, EnigmaProject project, Token token, Token movedToken, Translator translator) { + private String remapToken(TokenStore target, EnigmaProjectImpl project, Token token, Token movedToken, Translator translator) { EntryReference, Entry> reference = this.obfuscatedIndex.getReference(token); Entry entry = this.obfuscatedIndex.remapToNameable ? reference.getNameableEntry() : reference.entry; @@ -64,6 +65,10 @@ private String remapToken(TokenStore target, EnigmaProject project, Token token, } else { target.add(project, EntryMapping.OBFUSCATED, movedToken); } + } else if (project.isInternallyRenamable(reference)) { + if (translatedEntry != null && !translatedEntry.isObfuscated()) { + return translatedEntry.getValue().getSourceRemapName(); + } } else if (DEBUG_TOKEN_HIGHLIGHTS) { target.add(project, new EntryMapping(null, null, TokenType.DEBUG, null), movedToken); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java b/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java new file mode 100644 index 000000000..ba0a65d67 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/EnigmaProjectImpl.java @@ -0,0 +1,526 @@ +package org.quiltmc.enigma.impl; + +import com.google.common.base.Functions; +import com.google.common.base.Preconditions; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.EntryReference; +import org.quiltmc.enigma.api.analysis.index.jar.EnclosingMethodIndex; +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.mapping.MappingsIndex; +import org.quiltmc.enigma.api.class_provider.ClassProvider; +import org.quiltmc.enigma.api.class_provider.ObfuscationFixClassProvider; +import org.quiltmc.enigma.api.service.DecompilerService; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.service.ObfuscationTestService; +import org.quiltmc.enigma.api.source.Decompiler; +import org.quiltmc.enigma.api.source.SourceSettings; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.Translator; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.mapping.EntryResolver; +import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; +import org.quiltmc.enigma.api.translation.mapping.tree.DeltaTrackingTree; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTree; +import org.quiltmc.enigma.api.translation.mapping.tree.EntryTreeUtil; +import org.quiltmc.enigma.api.translation.mapping.tree.HashEntryTree; +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.LocalVariableEntry; +import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.impl.bytecode.translator.TranslationClassVisitor; +import org.quiltmc.enigma.impl.plugin.EnumConstantIndexingService; +import org.quiltmc.enigma.impl.translation.mapping.MappingsChecker; +import org.quiltmc.enigma.util.I18n; +import org.tinylog.Logger; + +import javax.annotation.Nullable; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EnigmaProjectImpl implements EnigmaProject { + private final Enigma enigma; + private final Path jarPath; + private final ClassProvider classProvider; + private final JarIndex jarIndex; + private final JarIndex libIndex; + private final JarIndex combinedIndex; + private final byte[] jarChecksum; + private final Map libraryMethodOverrideCache = new HashMap<>(); + + private EntryRemapper remapper; + private MappingsIndex mappingsIndex; + + public EnigmaProjectImpl(Enigma enigma, Path jarPath, ClassProvider classProvider, JarIndex jarIndex, JarIndex libIndex, JarIndex combinedIndex, 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.combinedIndex = combinedIndex; + this.jarChecksum = jarChecksum; + + this.mappingsIndex = mappingsIndex; + this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, proposedNames, new HashEntryTree<>(), this.enigma.getNameProposalServices()); + } + + @Override + public void setMappings(@Nullable EntryTree mappings, ProgressListener progress) { + // keep bytecode-based proposed names, to avoid unnecessary recalculation + EntryTree jarProposedMappings = this.remapper != null ? this.remapper.getJarProposedMappings() : new HashEntryTree<>(); + + this.mappingsIndex = MappingsIndex.empty(); + + if (mappings != null) { + EntryTree mergedTree = EntryTreeUtil.merge(jarProposedMappings, mappings); + + this.mappingsIndex.indexMappings(mergedTree, progress); + this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, mappings, this.enigma.getNameProposalServices()); + } else if (!jarProposedMappings.isEmpty()) { + this.mappingsIndex.indexMappings(jarProposedMappings, progress); + this.remapper = EntryRemapper.mapped(this.enigma, this.combinedIndex, this.mappingsIndex, jarProposedMappings, new HashEntryTree<>(), this.enigma.getNameProposalServices()); + } else { + this.remapper = EntryRemapper.empty(this.enigma, this.combinedIndex, this.enigma.getNameProposalServices()); + } + + // update dynamically proposed names + this.remapper.insertDynamicallyProposedMappings(null, null, null); + } + + @Override + public Enigma getEnigma() { + return this.enigma; + } + + @Override + public Path getJarPath() { + return this.jarPath; + } + + @Override + public ClassProvider getClassProvider() { + return this.classProvider; + } + + @Override + public JarIndex getJarIndex() { + return this.jarIndex; + } + + @Override + public JarIndex getLibIndex() { + return this.libIndex; + } + + @Override + public JarIndex getCombinedIndex() { + return this.combinedIndex; + } + + @Override + public MappingsIndex getMappingsIndex() { + return this.mappingsIndex; + } + + @Override + public byte[] getJarChecksum() { + return Arrays.copyOf(this.jarChecksum, this.jarChecksum.length); + } + + @Override + public EntryRemapper getRemapper() { + return this.remapper; + } + + @Override + public Collection> dropMappings(ProgressListener progress) { + DeltaTrackingTree mappings = this.remapper.getMappings(); + + Collection> dropped = this.dropMappings(mappings, progress); + for (Entry entry : dropped) { + mappings.trackChange(entry); + } + + return dropped; + } + + private Collection> dropMappings(EntryTree mappings, ProgressListener progress) { + MappingsChecker.Dropper dropper = new MappingsChecker.Dropper(); + + // drop mappings that don't match the jar + MappingsChecker checker = new MappingsChecker(this, this.jarIndex, mappings); + + checker.collectBrokenMappings(progress, dropper); + + Map, String> droppedBrokenMappings = dropper.getPendingDroppedMappings(); + for (Map.Entry, String> mapping : droppedBrokenMappings.entrySet()) { + Logger.warn("Couldn't find {} ({}) in jar. Mapping was dropped.", mapping.getKey(), mapping.getValue()); + } + + dropper.applyPendingDrops(mappings); + checker.collectEmptyMappings(progress, dropper); + + Map, String> droppedEmptyMappings = dropper.getPendingDroppedMappings(); + for (Map.Entry, String> mapping : droppedEmptyMappings.entrySet()) { + Logger.warn("{} ({}) was empty. Mapping was dropped.", mapping.getKey(), mapping.getValue()); + } + + dropper.applyPendingDrops(mappings); + + return dropper.getDroppedMappings().keySet(); + } + + @Override + public boolean isNavigable(Entry obfEntry) { + if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { + return false; + } + + return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); + } + + public boolean isInternallyRenamable(Entry obfEntry) { + if (obfEntry instanceof MethodEntry obfMethodEntry) { + // constructors are not renamable! + if (obfMethodEntry.isConstructor()) { + return false; + } + + // HACKHACK: Object methods are not obfuscated identifiers + String name = obfMethodEntry.getName(); + String sig = obfMethodEntry.getDesc().toString(); + + // 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() + ";")) + || isEnumValueOfMethod(parent, obfMethodEntry))) { + return false; + } + + if (this.isLibraryMethodOverride(obfMethodEntry)) { + return false; + } + } else if (obfEntry instanceof LocalVariableEntry localEntry) { + if (!localEntry.isArgument()) { + return false; + } + + MethodEntry method = localEntry.getParent(); + 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 (isEnumValueOfMethod(parent, method)) { + return false; + } + } else if (obfEntry instanceof ClassEntry classEntry && this.isAnonymousOrLocal(classEntry)) { + return false; + } + + return this.jarIndex.getIndex(EntryIndex.class).hasEntry(obfEntry); + } + + private boolean isLibraryMethodOverride(MethodEntry methodEntry) { + final Boolean cached = this.libraryMethodOverrideCache.get(methodEntry); + if (cached != null) { + return cached; + } else { + if (this.combinedIndex.getIndex(EntryIndex.class).hasMethod(methodEntry)) { + final EntryResolver combinedResolver = this.combinedIndex.getEntryResolver(); + final Set equivalents = combinedResolver.resolveEquivalentMethods(methodEntry); + final Set roots = equivalents.stream() + .flatMap(equivalent -> combinedResolver.resolveEntry(equivalent, ResolutionStrategy.RESOLVE_ROOT).stream()) + .collect(Collectors.toSet()); + + final Set equivalentsAndRoots = Stream + .concat(equivalents.stream(), roots.stream()) + .collect(Collectors.toSet()); + + final EntryIndex jarEntryIndex = this.jarIndex.getIndex(EntryIndex.class); + final boolean anyNonJar = equivalentsAndRoots.stream().anyMatch(method -> !jarEntryIndex.hasMethod(method)); + + equivalentsAndRoots.forEach(method -> this.libraryMethodOverrideCache.put(method, anyNonJar)); + + return anyNonJar; + } else { + this.libraryMethodOverrideCache.put(methodEntry, false); + + return false; + } + } + } + + @Override + public boolean isRenamable(Entry obfEntry) { + if (this.isInternallyRenamable(obfEntry)) { + if (obfEntry instanceof FieldEntry fieldEntry) { + return !this.getEnumConstantIndexingService() + .map(service -> service.isEnumConstant(fieldEntry)) + .orElse(false); + } else { + return true; + } + } else { + return false; + } + } + + private Optional getEnumConstantIndexingService() { + return this.getEnigma() + .getService(JarIndexerService.TYPE, EnumConstantIndexingService.ID) + .map(service -> (EnumConstantIndexingService) service); + } + + 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 isInternallyRenamable(EntryReference, Entry> obfReference) { + return obfReference.isNamed() && this.isInternallyRenamable(obfReference.getNameableEntry()); + } + + @Override + public boolean isRenamable(EntryReference, Entry> obfReference) { + return obfReference.isNamed() && this.isRenamable(obfReference.getNameableEntry()); + } + + @Override + public boolean isObfuscated(Entry entry) { + List obfuscationTestServices = this.getEnigma().getServices().get(ObfuscationTestService.TYPE); + if (!obfuscationTestServices.isEmpty()) { + for (ObfuscationTestService service : obfuscationTestServices) { + if (service.testDeobfuscated(entry)) { + return false; + } + } + } + + EntryMapping mapping = this.remapper.getMapping(entry); + return mapping.tokenType() == TokenType.OBFUSCATED; + } + + @Override + public boolean isSynthetic(Entry entry) { + return this.jarIndex.getIndex(EntryIndex.class).hasEntry(entry) && this.jarIndex.getIndex(EntryIndex.class).getEntryAccess(entry).isSynthetic(); + } + + @Override + public boolean isAnonymousOrLocal(ClassEntry classEntry) { + EnclosingMethodIndex enclosingMethodIndex = this.jarIndex.getIndex(EnclosingMethodIndex.class); + // Only local and anonymous classes may have the EnclosingMethod attribute + return enclosingMethodIndex.hasEnclosingMethod(classEntry); + } + + @Override + @SuppressWarnings("DataFlowIssue") + public boolean validateParameterIndex(LocalVariableEntry parameter) { + MethodEntry parent = parameter.getParent(); + EntryIndex index = this.jarIndex.getIndex(EntryIndex.class); + + if (index.hasMethod(parent)) { + AtomicInteger maxLocals = new AtomicInteger(-1); + ClassEntry parentClass = parent.getParent(); + + // find max_locals for method, representing the number of parameters it receives (JVMS§4.7.3) + // note: parent class cannot be null, warning suppressed + ClassNode classNode = this.getClassProvider().get(parentClass.getFullName()); + if (classNode != null) { + classNode.methods.stream() + .filter(node -> node.name.equals(parent.getName()) && node.desc.equals(parent.getDesc().toString())) + .findFirst().ifPresent(node -> { + // occasionally it's possible to run into a method that has parameters, yet whose max locals is 0. java is stupid. we ignore those cases + if (!(node.parameters != null && node.parameters.size() > node.maxLocals)) { + maxLocals.set(node.maxLocals); + } + }); + } + + // if maxLocals is -1 it's not found for the method and should be ignored + return index.validateParameterIndex(parameter) && (maxLocals.get() == -1 || parameter.getIndex() <= maxLocals.get() - 1); + } + + return false; + } + + @Override + public JarExportImpl exportRemappedJar(ProgressListener progress) { + Collection classEntries = this.jarIndex.getIndex(EntryIndex.class).getClasses(); + ClassProvider fixingClassProvider = new ObfuscationFixClassProvider(this.classProvider, this.jarIndex); + Translator deobfuscator = this.remapper.getDeobfuscator(); + + AtomicInteger count = new AtomicInteger(); + progress.init(classEntries.size(), I18n.translate("progress.classes.deobfuscating")); + + Map compiled = classEntries.parallelStream() + .map(entry -> { + ClassEntry translatedEntry = deobfuscator.translate(entry); + progress.step(count.getAndIncrement(), translatedEntry.toString()); + + ClassNode node = fixingClassProvider.get(entry.getFullName()); + if (node != null) { + ClassNode translatedNode = new ClassNode(); + node.accept(new TranslationClassVisitor(deobfuscator, Enigma.ASM_VERSION, translatedNode)); + return translatedNode; + } + + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(n -> n.name, Functions.identity())); + + return new JarExportImpl(this.remapper, compiled); + } + + public static final class JarExportImpl implements JarExport { + private final EntryRemapper mapper; + private final Map compiled; + + JarExportImpl(EntryRemapper mapper, Map compiled) { + this.mapper = mapper; + this.compiled = compiled; + } + + public void write(Path path, ProgressListener progress) throws IOException { + progress.init(this.compiled.size(), I18n.translate("progress.jar.writing")); + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(path))) { + AtomicInteger count = new AtomicInteger(); + + for (ClassNode node : this.compiled.values()) { + progress.step(count.getAndIncrement(), node.name); + + String entryName = node.name.replace('.', '/') + ".class"; + + ClassWriter writer = new ClassWriter(0); + node.accept(writer); + + out.putNextEntry(new JarEntry(entryName)); + out.write(writer.toByteArray()); + out.closeEntry(); + } + } + } + + public SourceExport decompile(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { + List decompiled = this.decompileStream(progress, decompilerService, errorStrategy).toList(); + return new SourceExportImpl(decompiled); + } + + public Stream decompileStream(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) { + Collection classes = this.compiled.values().stream() + .filter(classNode -> classNode.name.indexOf('$') == -1) + .toList(); + + progress.init(classes.size(), I18n.translate("progress.classes.decompiling")); + + //create a common instance outside the loop as mappings shouldn't be changing while this is happening + Decompiler decompiler = decompilerService.create(ClassProvider.fromMap(this.compiled), new SourceSettings(false, false)); + + AtomicInteger count = new AtomicInteger(); + + return classes.parallelStream() + .map(translatedNode -> { + progress.step(count.getAndIncrement(), translatedNode.name); + + String source = null; + try { + source = this.decompileClass(translatedNode, decompiler); + } catch (Exception e) { + switch (errorStrategy) { + case PROPAGATE: throw e; + case IGNORE: break; + case TRACE_AS_SOURCE: { + StringWriter writer = new StringWriter(); + e.printStackTrace(new PrintWriter(writer)); + source = writer.toString(); + break; + } + } + } + + if (source == null) { + return null; + } + + return new ClassSourceImpl(translatedNode.name, source); + }) + .filter(Objects::nonNull); + } + + private String decompileClass(ClassNode translatedNode, Decompiler decompiler) { + return decompiler.getSource(translatedNode.name, this.mapper).asString(); + } + } + + public static final class SourceExportImpl implements SourceExport { + public final Collection decompiled; + + SourceExportImpl(Collection decompiled) { + this.decompiled = decompiled; + } + + public void write(Path path, ProgressListener progress) throws IOException { + progress.init(this.decompiled.size(), I18n.translate("progress.sources.writing")); + + int count = 0; + for (ClassSourceImpl source : this.decompiled) { + progress.step(count++, source.name); + + Path sourcePath = source.resolvePath(path); + source.writeTo(sourcePath); + } + } + } + + public static class ClassSourceImpl implements ClassSource { + public final String name; + public final String source; + + ClassSourceImpl(String name, String source) { + this.name = name; + this.source = source; + } + + public void writeTo(Path path) throws IOException { + Files.createDirectories(path.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(path)) { + writer.write(this.source); + } + } + + public Path resolvePath(Path root) { + return root.resolve(this.name.replace('.', '/') + ".java"); + } + } +} 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 63f591372..975849eed 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 @@ -4,7 +4,6 @@ import org.quiltmc.enigma.api.analysis.index.jar.BridgeMethodIndex; import org.quiltmc.enigma.api.EnigmaPlugin; import org.quiltmc.enigma.api.EnigmaPluginContext; -import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.service.JarIndexerService; import org.quiltmc.enigma.api.service.NameProposalService; @@ -30,35 +29,10 @@ public void init(EnigmaPluginContext ctx) { } private static void registerEnumNamingService(EnigmaPluginContext ctx) { - final Map, String> names = new HashMap<>(); - final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); + final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(); - ctx.registerService(JarIndexerService.TYPE, ctx1 -> JarIndexerService.fromVisitor(visitor, "enigma:enum_initializer_indexer")); - - ctx.registerService(NameProposalService.TYPE, ctx1 -> new NameProposalService() { - @Override - public Map, EntryMapping> getProposedNames(Enigma enigma, JarIndex index) { - Map, EntryMapping> mappings = new HashMap<>(); - - index.getIndex(EntryIndex.class).getFields().forEach(field -> { - if (names.containsKey(field)) { - mappings.put(field, this.createMapping(names.get(field), TokenType.JAR_PROPOSED)); - } - }); - - return mappings; - } - - @Override - public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { - return null; - } - - @Override - public String getId() { - return "enigma:enum_name_proposer"; - } - }); + ctx.registerService(JarIndexerService.TYPE, ctx1 -> new EnumConstantIndexingService(visitor)); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new EnumConstantProposalService(visitor)); } private static void registerRecordNamingService(EnigmaPluginContext ctx) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java new file mode 100644 index 000000000..d92445943 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantIndexingService.java @@ -0,0 +1,44 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.objectweb.asm.tree.ClassNode; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.class_provider.ProjectClassProvider; +import org.quiltmc.enigma.api.service.JarIndexerService; +import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; + +import javax.annotation.Nullable; +import java.util.Set; + +public class EnumConstantIndexingService implements JarIndexerService { + public static final String ID = "enigma:enum_initializer_indexer"; + + private final EnumFieldNameFindingVisitor visitor; + + EnumConstantIndexingService(EnumFieldNameFindingVisitor visitor) { + this.visitor = visitor; + } + + @Override + public void acceptJar(Set scope, ProjectClassProvider classProvider, JarIndex jarIndex) { + for (String className : scope) { + ClassNode node = classProvider.get(className); + if (node != null) { + node.accept(this.visitor); + } + } + } + + @Override + public String getId() { + return ID; + } + + public boolean isEnumConstant(FieldEntry field) { + return this.visitor.isEnumConstant(field); + } + + @Nullable + public String getEnumConstantName(FieldEntry field) { + return this.visitor.getEnumConstantName(field); + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java new file mode 100644 index 000000000..67eb68acc --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumConstantProposalService.java @@ -0,0 +1,49 @@ +package org.quiltmc.enigma.impl.plugin; + +import org.quiltmc.enigma.api.Enigma; +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.Entry; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +public class EnumConstantProposalService implements NameProposalService { + private final EnumFieldNameFindingVisitor visitor; + + EnumConstantProposalService(EnumFieldNameFindingVisitor visitor) { + this.visitor = visitor; + } + + @Override + public Map, EntryMapping> getProposedNames(Enigma enigma, JarIndex index) { + Map, EntryMapping> mappings = new HashMap<>(); + + index.getIndex(EntryIndex.class).getFields().forEach(field -> { + final String name = this.visitor.getEnumConstantName(field); + if (name != null) { + mappings.put(field, this.createMapping(name, TokenType.JAR_PROPOSED)); + } + }); + + return mappings; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames( + EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, + @Nullable EntryMapping newMapping + ) { + return null; + } + + @Override + public String getId() { + return "enigma:enum_name_proposer"; + } +} diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java index a9e5baeba..fa706c276 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/plugin/EnumFieldNameFindingVisitor.java @@ -18,10 +18,11 @@ import org.quiltmc.enigma.api.Enigma; 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.Entry; import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; +import javax.annotation.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -30,13 +31,13 @@ final class EnumFieldNameFindingVisitor extends ClassVisitor { private ClassEntry clazz; private String className; - private final Map, String> mappings; + private final Map enumConstants; private final Set> enumFields = new HashSet<>(); private final List classInits = new ArrayList<>(); - EnumFieldNameFindingVisitor(Map, String> mappings) { + EnumFieldNameFindingVisitor() { super(Enigma.ASM_VERSION); - this.mappings = mappings; + this.enumConstants = new HashMap<>(); } @Override @@ -44,8 +45,6 @@ public void visit(int version, int access, String name, String signature, String super.visit(version, access, name, signature, superName, interfaces); this.className = name; this.clazz = new ClassEntry(name); - this.enumFields.clear(); - this.classInits.clear(); } @Override @@ -76,9 +75,21 @@ public void visitEnd() { this.collectResults(); } catch (Exception ex) { throw new RuntimeException(ex); + } finally { + this.enumFields.clear(); + this.classInits.clear(); } } + public boolean isEnumConstant(FieldEntry field) { + return this.enumConstants.containsKey(field); + } + + @Nullable + public String getEnumConstantName(FieldEntry field) { + return this.enumConstants.get(field); + } + private void collectResults() throws Exception { String owner = this.className; Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); @@ -108,7 +119,7 @@ private void collectResults() throws Exception { } if (s != null) { - this.mappings.put(new FieldEntry(this.clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); + this.enumConstants.put(new FieldEntry(this.clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); } // report otherwise? diff --git a/enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java b/enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java new file mode 100644 index 000000000..868d1adf5 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/input/z_parameter_connection/ParameterConnections.java @@ -0,0 +1,12 @@ +package org.quiltmc.enigma.input.z_parameter_connection; + +public class ParameterConnections { + static Object toStringOf(String param) { + return new Object() { + @Override + public String toString() { + return param; + } + }; + } +}