From d511379c295a5f5f8c8f9900c18aee4dde6a8674 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 11 Sep 2025 09:42:32 +0100 Subject: Allow plugins to be able to translate entries from deobfuscated to obfuscated as well --- .../src/main/java/cuchaz/enigma/EnigmaProject.java | 57 ++++++++-- .../java/cuchaz/enigma/api/view/ProjectView.java | 7 ++ .../enigma/translation/ObfuscatingTranslator.java | 115 +++++++++++++++++++++ .../java/cuchaz/enigma/translation/Translator.java | 2 + .../enigma/translation/mapping/EntryRemapper.java | 10 +- .../enigma/translation/mapping/EntryUtil.java | 10 +- .../representation/entry/FieldEntry.java | 4 + .../representation/entry/MethodEntry.java | 4 + 8 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 enigma/src/main/java/cuchaz/enigma/translation/ObfuscatingTranslator.java (limited to 'enigma') diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java index 4a528a7..86a7037 100644 --- a/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java +++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProject.java @@ -38,9 +38,11 @@ import cuchaz.enigma.classprovider.ObfuscationFixClassProvider; import cuchaz.enigma.source.Decompiler; import cuchaz.enigma.source.DecompilerService; import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.translation.ObfuscatingTranslator; import cuchaz.enigma.translation.ProposingTranslator; import cuchaz.enigma.translation.Translatable; import cuchaz.enigma.translation.Translator; +import cuchaz.enigma.translation.mapping.EntryChange; import cuchaz.enigma.translation.mapping.EntryMapping; import cuchaz.enigma.translation.mapping.EntryRemapper; import cuchaz.enigma.translation.mapping.MappingsChecker; @@ -63,6 +65,9 @@ public class EnigmaProject implements ProjectView { private final Set projectClasses; private EntryRemapper mapper; + private Translator proposingTranslator; + @Nullable + private ObfuscatingTranslator inverseTranslator; private final List dataInvalidationListeners = new ArrayList<>(); @@ -78,7 +83,7 @@ public class EnigmaProject implements ProjectView { this.jarChecksum = jarChecksum; this.projectClasses = projectClasses; - this.mapper = EntryRemapper.empty(jarIndex); + setMappings(null); } public void setMappings(EntryTree mappings) { @@ -87,6 +92,13 @@ public class EnigmaProject implements ProjectView { } else { mapper = EntryRemapper.empty(jarIndex); } + + NameProposalService[] nameProposalServices = enigma.getServices().get(NameProposalService.TYPE).toArray(new NameProposalService[0]); + proposingTranslator = nameProposalServices.length == 0 ? mapper.getDeobfuscator() : new ProposingTranslator(mapper, nameProposalServices); + + if (inverseTranslator != null) { + inverseTranslator.refreshAll(proposingTranslator); + } } public Enigma getEnigma() { @@ -121,6 +133,10 @@ public class EnigmaProject implements ProjectView { for (Entry entry : dropped) { mappings.trackChange(entry); } + + if (inverseTranslator != null) { + inverseTranslator.refreshAll(proposingTranslator); + } } private Collection> dropMappings(EntryTree mappings, ProgressListener progress) { @@ -224,21 +240,18 @@ public class EnigmaProject implements ProjectView { Collection classEntries = jarIndex.getEntryIndex().getClasses(); ClassProvider fixingClassProvider = new ObfuscationFixClassProvider(classProvider, jarIndex); - NameProposalService[] nameProposalServices = getEnigma().getServices().get(NameProposalService.TYPE).toArray(new NameProposalService[0]); - Translator deobfuscator = nameProposalServices.length == 0 ? mapper.getDeobfuscator() : new ProposingTranslator(mapper, nameProposalServices); - AtomicInteger count = new AtomicInteger(); progress.init(classEntries.size(), I18n.translate("progress.classes.deobfuscating")); Map compiled = classEntries.parallelStream().map(entry -> { - ClassEntry translatedEntry = deobfuscator.translate(entry); + ClassEntry translatedEntry = proposingTranslator.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)); + node.accept(new TranslationClassVisitor(proposingTranslator, Enigma.ASM_VERSION, translatedNode)); return translatedNode; } @@ -348,7 +361,37 @@ public class EnigmaProject implements ProjectView { @Override @SuppressWarnings("unchecked") public T deobfuscate(T entry) { - return (T) mapper.extendedDeobfuscate((Translatable) entry).getValue(); + return (T) proposingTranslator.extendedTranslate((Translatable) entry).getValue(); + } + + @Override + @SuppressWarnings("unchecked") + public T obfuscate(T entry) { + if (inverseTranslator == null) { + throw new IllegalStateException("Must call registerForInverseMappings before calling obfuscate"); + } + + return (T) inverseTranslator.extendedTranslate((Entry) entry).getValue(); + } + + @Override + public void registerForInverseMappings() { + if (inverseTranslator == null) { + inverseTranslator = new ObfuscatingTranslator(jarIndex); + inverseTranslator.refreshAll(proposingTranslator); + } + } + + public void onEntryChange(EntryMapping prevMapping, EntryChange change) { + if (inverseTranslator == null || change.getDeobfName().isUnchanged()) { + return; + } + + String newName = change.getDeobfName().isSet() ? change.getDeobfName().getNewValue() : proposingTranslator.extendedTranslate(change.getTarget()).getValue().getName(); + + for (Entry equivalentEntry : mapper.getObfResolver().resolveEquivalentEntries(change.getTarget())) { + inverseTranslator.refreshName(equivalentEntry, prevMapping.targetName(), newName); + } } @Override diff --git a/enigma/src/main/java/cuchaz/enigma/api/view/ProjectView.java b/enigma/src/main/java/cuchaz/enigma/api/view/ProjectView.java index b8760e2..8f53d11 100644 --- a/enigma/src/main/java/cuchaz/enigma/api/view/ProjectView.java +++ b/enigma/src/main/java/cuchaz/enigma/api/view/ProjectView.java @@ -13,6 +13,13 @@ import cuchaz.enigma.api.view.entry.EntryView; public interface ProjectView { T deobfuscate(T entry); + /** + * Must call {@link #registerForInverseMappings()} before using this method. + */ + T obfuscate(T entry); + + void registerForInverseMappings(); + Collection getProjectClasses(); @Nullable diff --git a/enigma/src/main/java/cuchaz/enigma/translation/ObfuscatingTranslator.java b/enigma/src/main/java/cuchaz/enigma/translation/ObfuscatingTranslator.java new file mode 100644 index 0000000..27220e0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/translation/ObfuscatingTranslator.java @@ -0,0 +1,115 @@ +package cuchaz.enigma.translation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import cuchaz.enigma.analysis.index.JarIndex; +import cuchaz.enigma.translation.mapping.EntryMapping; +import cuchaz.enigma.translation.mapping.EntryResolver; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.mapping.tree.EntryTree; +import cuchaz.enigma.translation.mapping.tree.HashEntryTree; +import cuchaz.enigma.translation.representation.entry.ClassEntry; +import cuchaz.enigma.translation.representation.entry.Entry; +import cuchaz.enigma.translation.representation.entry.FieldEntry; +import cuchaz.enigma.translation.representation.entry.MethodEntry; +import cuchaz.enigma.translation.representation.entry.ParentedEntry; + +public class ObfuscatingTranslator implements Translator { + private final JarIndex jarIndex; + + private final EntryTree inverseMappings = new HashEntryTree<>(); + private final EntryResolver resolver = new ObfuscatingResolver(); + + public ObfuscatingTranslator(JarIndex jarIndex) { + this.jarIndex = jarIndex; + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public TranslateResult extendedTranslate(@Nullable T translatable) { + if (translatable == null) { + return null; + } + + if (translatable instanceof FieldEntry || translatable instanceof MethodEntry) { + ParentedEntry key = obfOwnerAndDesc((ParentedEntry) translatable); + EntryMapping mapping = inverseMappings.get(key); + return mapping == null ? TranslateResult.obfuscated((T) key) : TranslateResult.deobfuscated((T) key.withName(mapping.targetName())); + } + + return (TranslateResult) translatable.extendedTranslate(this, resolver, inverseMappings); + } + + public void refreshAll(Translator deobfuscator) { + inverseMappings.clear(); + + for (ClassEntry clazz : jarIndex.getEntryIndex().getClasses()) { + inverseMappings.insert(deobfuscator.extendedTranslate(clazz).getValue(), new EntryMapping(clazz.getName())); + } + + for (FieldEntry field : jarIndex.getEntryIndex().getFields()) { + inverseMappings.insert(obfOwnerAndDesc(deobfuscator.extendedTranslate(field).getValue()), new EntryMapping(field.getName())); + } + + for (MethodEntry method : jarIndex.getEntryIndex().getMethods()) { + inverseMappings.insert(obfOwnerAndDesc(deobfuscator.extendedTranslate(method).getValue()), new EntryMapping(method.getName())); + } + } + + public void refreshName(Entry entry, String oldDeobfName, String newDeobfName) { + inverseMappings.remove(entry.withName(oldDeobfName)); + inverseMappings.insert(entry.withName(newDeobfName), new EntryMapping(entry.getName())); + } + + @SuppressWarnings("unchecked") + private > T obfOwnerAndDesc(T translatable) { + if (translatable.getParent() != null) { + translatable = (T) translatable.withParent(extendedTranslate(translatable.getParent()).getValue()); + } + + if (translatable instanceof FieldEntry field) { + translatable = (T) field.withDesc(extendedTranslate(field.getDesc()).getValue()); + } else if (translatable instanceof MethodEntry method) { + translatable = (T) method.withDesc(extendedTranslate(method.getDesc()).getValue()); + } + + return translatable; + } + + private class ObfuscatingResolver implements EntryResolver { + @Override + @SuppressWarnings("unchecked") + public > Collection resolveEntry(E entry, ResolutionStrategy strategy) { + ClassEntry containingClass; + + if (entry instanceof ClassEntry || (containingClass = entry.findAncestor(ClassEntry.class)) == null) { + return List.of(entry); + } + + List result = new ArrayList<>(); + result.add(entry); + + for (ClassEntry parentClass : jarIndex.getInheritanceIndex().getAncestors(containingClass)) { + result.add((E) entry.replaceAncestor(containingClass, parentClass)); + } + + return result; + } + + @Override + public Set> resolveEquivalentEntries(Entry entry) { + return Set.of(entry); + } + + @Override + public Set resolveEquivalentMethods(MethodEntry methodEntry) { + return Set.of(methodEntry); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/translation/Translator.java b/enigma/src/main/java/cuchaz/enigma/translation/Translator.java index 5688990..593b4ef 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/Translator.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/Translator.java @@ -18,10 +18,12 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; public interface Translator { @Nullable + @Contract("null -> null; !null -> !null") TranslateResult extendedTranslate(@Nullable T translatable); @Deprecated diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java index 12ed7b1..dea2d15 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryRemapper.java @@ -54,11 +54,11 @@ public class EntryRemapper { doPutMapping(vc, obfuscatedEntry, deobfMapping, true); } - public void putMapping(ValidationContext vc, Entry obfuscatedEntry, @NotNull EntryMapping deobfMapping) { - doPutMapping(vc, obfuscatedEntry, deobfMapping, false); + public boolean putMapping(ValidationContext vc, Entry obfuscatedEntry, @NotNull EntryMapping deobfMapping) { + return doPutMapping(vc, obfuscatedEntry, deobfMapping, false); } - private void doPutMapping(ValidationContext vc, Entry obfuscatedEntry, @NotNull EntryMapping deobfMapping, boolean validateOnly) { + private boolean doPutMapping(ValidationContext vc, Entry obfuscatedEntry, @NotNull EntryMapping deobfMapping, boolean validateOnly) { if (obfuscatedEntry instanceof FieldEntry) { FieldEntry fieldEntry = (FieldEntry) obfuscatedEntry; ClassEntry classEntry = fieldEntry.getParent(); @@ -77,7 +77,7 @@ public class EntryRemapper { } if (validateOnly || !vc.canProceed()) { - return; + return false; } for (Entry resolvedEntry : resolvedEntries) { @@ -87,6 +87,8 @@ public class EntryRemapper { obfToDeobf.insert(resolvedEntry, deobfMapping); } } + + return true; } // A little bit of a hack to also map the getter method for record fields. diff --git a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryUtil.java b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryUtil.java index fb8ab63..9e6641b 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryUtil.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/mapping/EntryUtil.java @@ -1,17 +1,23 @@ package cuchaz.enigma.translation.mapping; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import cuchaz.enigma.EnigmaProject; import cuchaz.enigma.translation.representation.entry.Entry; import cuchaz.enigma.utils.validation.ValidationContext; public class EntryUtil { - public static EntryMapping applyChange(ValidationContext vc, EntryRemapper remapper, EntryChange change) { + public static EntryMapping applyChange(ValidationContext vc, @Nullable EnigmaProject project, EntryRemapper remapper, EntryChange change) { Entry target = change.getTarget(); EntryMapping prev = remapper.getDeobfMapping(target); EntryMapping mapping = EntryUtil.applyChange(prev, change); - remapper.putMapping(vc, target, mapping); + if (remapper.putMapping(vc, target, mapping)) { + if (project != null) { + project.onEntryChange(prev, change); + } + } return mapping; } diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java index dba8644..90c256c 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/FieldEntry.java @@ -58,6 +58,10 @@ public class FieldEntry extends ParentedEntry implements Comparable< return new FieldEntry(parent, name, desc, null); } + public FieldEntry withDesc(TypeDescriptor desc) { + return new FieldEntry(parent, name, desc, null); + } + @Override public FieldEntry withParent(ClassEntry parent) { return new FieldEntry(parent, this.name, this.desc, null); diff --git a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java index d86f156..da6751e 100644 --- a/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java +++ b/enigma/src/main/java/cuchaz/enigma/translation/representation/entry/MethodEntry.java @@ -69,6 +69,10 @@ public class MethodEntry extends ParentedEntry implements Comparable return new MethodEntry(parent, name, descriptor, javadocs); } + public MethodEntry withDesc(MethodDescriptor descriptor) { + return new MethodEntry(parent, name, descriptor, javadocs); + } + @Override public MethodEntry withParent(ClassEntry parent) { return new MethodEntry(new ClassEntry(parent.getFullName()), name, descriptor, javadocs); -- cgit v1.2.3