From ad59e46740ef636b95667615e3881fcee6fbbcb9 Mon Sep 17 00:00:00 2001 From: liach Date: Fri, 8 Nov 2019 16:35:19 -0600 Subject: Allow multiple services for enigma (#168) * Allow multiple services for enigma Signed-off-by: liach * Delete bad dummy Signed-off-by: liach --- src/main/java/cuchaz/enigma/Enigma.java | 29 ++-- src/main/java/cuchaz/enigma/EnigmaProfile.java | 61 ++++++++- src/main/java/cuchaz/enigma/EnigmaProject.java | 7 +- src/main/java/cuchaz/enigma/EnigmaServices.java | 14 +- src/main/java/cuchaz/enigma/Main.java | 11 +- .../java/cuchaz/enigma/ProposingTranslator.java | 74 +++++----- .../java/cuchaz/enigma/analysis/BuiltinPlugin.java | 150 +++++++++++++++++++++ .../cuchaz/enigma/gui/DecompiledClassSource.java | 12 +- src/main/java/cuchaz/enigma/gui/Gui.java | 2 + src/main/java/cuchaz/enigma/gui/GuiController.java | 6 +- 10 files changed, 292 insertions(+), 74 deletions(-) create mode 100644 src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java (limited to 'src/main/java/cuchaz/enigma') diff --git a/src/main/java/cuchaz/enigma/Enigma.java b/src/main/java/cuchaz/enigma/Enigma.java index fd23b47..4522ed7 100644 --- a/src/main/java/cuchaz/enigma/Enigma.java +++ b/src/main/java/cuchaz/enigma/Enigma.java @@ -12,7 +12,10 @@ package cuchaz.enigma; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; import cuchaz.enigma.analysis.ClassCache; import cuchaz.enigma.analysis.index.JarIndex; import cuchaz.enigma.api.EnigmaPlugin; @@ -24,6 +27,10 @@ import cuchaz.enigma.api.service.JarIndexerService; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.ServiceLoader; public class Enigma { @@ -47,9 +54,7 @@ public class Enigma { ClassCache classCache = ClassCache.of(path); JarIndex jarIndex = classCache.index(progress); - services.get(JarIndexerService.TYPE).ifPresent(indexer -> { - indexer.acceptJar(classCache, jarIndex); - }); + services.get(JarIndexerService.TYPE).forEach(indexer -> indexer.acceptJar(classCache, jarIndex)); return new EnigmaProject(this, classCache, jarIndex); } @@ -95,7 +100,7 @@ public class Enigma { private static class PluginContext implements EnigmaPluginContext { private final EnigmaProfile profile; - private final ImmutableMap.Builder, EnigmaService> services = ImmutableMap.builder(); + private final ImmutableListMultimap.Builder, EnigmaService> services = ImmutableListMultimap.builder(); PluginContext(EnigmaProfile profile) { this.profile = profile; @@ -103,13 +108,15 @@ public class Enigma { @Override public void registerService(String id, EnigmaServiceType serviceType, EnigmaServiceFactory factory) { - EnigmaProfile.Service serviceProfile = profile.getServiceProfile(serviceType); - - // if this service type is not configured, or it is configured to use a different service id, skip - if (serviceProfile == null || !serviceProfile.matches(id)) return; - - T service = factory.create(serviceProfile::getArgument); - services.put(serviceType, service); + List serviceProfiles = profile.getServiceProfiles(serviceType); + + for (EnigmaProfile.Service serviceProfile : serviceProfiles) { + if (serviceProfile.matches(id)) { + T service = factory.create(serviceProfile::getArgument); + services.put(serviceType, service); + break; + } + } } EnigmaServices buildServices() { diff --git a/src/main/java/cuchaz/enigma/EnigmaProfile.java b/src/main/java/cuchaz/enigma/EnigmaProfile.java index 32f31e3..5a68be1 100644 --- a/src/main/java/cuchaz/enigma/EnigmaProfile.java +++ b/src/main/java/cuchaz/enigma/EnigmaProfile.java @@ -2,29 +2,42 @@ package cuchaz.enigma; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; import cuchaz.enigma.api.service.EnigmaServiceType; import cuchaz.enigma.translation.mapping.MappingFileNameFormat; import cuchaz.enigma.translation.mapping.MappingSaveParameters; -import javax.annotation.Nullable; import java.io.Reader; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; public final class EnigmaProfile { - public static final EnigmaProfile EMPTY = new EnigmaProfile(ImmutableMap.of()); + public static final EnigmaProfile EMPTY = new EnigmaProfile(new ServiceContainer(ImmutableMap.of())); private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF); - private static final Gson GSON = new Gson(); + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(ServiceContainer.class, (JsonDeserializer) EnigmaProfile::loadServiceContainer) + .create(); + private static final Type SERVICE_LIST_TYPE = new TypeToken>() { + }.getType(); @SerializedName("services") - private final Map serviceProfiles; + private final ServiceContainer serviceProfiles; @SerializedName("mapping_save_parameters") private final MappingSaveParameters mappingSaveParameters = null; - private EnigmaProfile(Map serviceProfiles) { + private EnigmaProfile(ServiceContainer serviceProfiles) { this.serviceProfiles = serviceProfiles; } @@ -32,8 +45,30 @@ public final class EnigmaProfile { return GSON.fromJson(reader, EnigmaProfile.class); } - @Nullable - public Service getServiceProfile(EnigmaServiceType serviceType) { + private static ServiceContainer loadServiceContainer(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!json.isJsonObject()) { + throw new JsonParseException("services must be an Object!"); + } + + JsonObject object = json.getAsJsonObject(); + + ImmutableMap.Builder> builder = ImmutableMap.builder(); + + for (Map.Entry entry : object.entrySet()) { + JsonElement value = entry.getValue(); + if (value.isJsonObject()) { + builder.put(entry.getKey(), Collections.singletonList(GSON.fromJson(value, Service.class))); + } else if (value.isJsonArray()) { + builder.put(entry.getKey(), GSON.fromJson(value, SERVICE_LIST_TYPE)); + } else { + throw new JsonParseException(String.format("Don't know how to convert %s to a list of service!", value)); + } + } + + return new ServiceContainer(builder.build()); + } + + public List getServiceProfiles(EnigmaServiceType serviceType) { return serviceProfiles.get(serviceType.key); } @@ -59,4 +94,16 @@ public final class EnigmaProfile { return args != null ? Optional.ofNullable(args.get(key)) : Optional.empty(); } } + + static final class ServiceContainer { + private final Map> services; + + ServiceContainer(Map> services) { + this.services = services; + } + + List get(String key) { + return services.getOrDefault(key, Collections.emptyList()); + } + } } diff --git a/src/main/java/cuchaz/enigma/EnigmaProject.java b/src/main/java/cuchaz/enigma/EnigmaProject.java index a589c63..47a3790 100644 --- a/src/main/java/cuchaz/enigma/EnigmaProject.java +++ b/src/main/java/cuchaz/enigma/EnigmaProject.java @@ -144,11 +144,8 @@ public class EnigmaProject { public JarExport exportRemappedJar(ProgressListener progress) { Collection classEntries = jarIndex.getEntryIndex().getClasses(); - Translator deobfuscator = getEnigma() - .getServices() - .get(NameProposalService.TYPE) - .map(nameProposalService -> (Translator) new ProposingTranslator(mapper, nameProposalService)) - .orElse(mapper.getDeobfuscator()); + 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(), "Deobfuscating classes..."); diff --git a/src/main/java/cuchaz/enigma/EnigmaServices.java b/src/main/java/cuchaz/enigma/EnigmaServices.java index 86507bc..45062d7 100644 --- a/src/main/java/cuchaz/enigma/EnigmaServices.java +++ b/src/main/java/cuchaz/enigma/EnigmaServices.java @@ -1,21 +1,21 @@ package cuchaz.enigma; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableListMultimap; import cuchaz.enigma.api.service.EnigmaService; import cuchaz.enigma.api.service.EnigmaServiceType; -import java.util.Optional; +import java.util.Collections; +import java.util.List; public final class EnigmaServices { - private final ImmutableMap, EnigmaService> services; + private final ImmutableListMultimap, EnigmaService> services; - EnigmaServices(ImmutableMap, EnigmaService> services) { + EnigmaServices(ImmutableListMultimap, EnigmaService> services) { this.services = services; } @SuppressWarnings("unchecked") - public Optional get(EnigmaServiceType type) { - EnigmaService service = services.get(type); - return Optional.ofNullable((T) service); + public List get(EnigmaServiceType type) { + return (List) services.get(type); } } diff --git a/src/main/java/cuchaz/enigma/Main.java b/src/main/java/cuchaz/enigma/Main.java index b778946..dbbcee4 100644 --- a/src/main/java/cuchaz/enigma/Main.java +++ b/src/main/java/cuchaz/enigma/Main.java @@ -18,6 +18,8 @@ import joptsimple.*; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -49,12 +51,19 @@ public class Main { return; } - EnigmaProfile parsedProfile = EnigmaProfile.EMPTY; + EnigmaProfile parsedProfile; if (options.has(profile)) { Path profilePath = options.valueOf(profile); try (BufferedReader reader = Files.newBufferedReader(profilePath)) { parsedProfile = EnigmaProfile.parse(reader); } + } else { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(Main.class.getResourceAsStream("/profile.json"), StandardCharsets.UTF_8))){ + parsedProfile = EnigmaProfile.parse(reader); + } catch (IOException ex) { + System.out.println("Failed to load default profile, will use empty profile: " + ex.getMessage()); + parsedProfile = EnigmaProfile.EMPTY; + } } Gui gui = new Gui(parsedProfile); diff --git a/src/main/java/cuchaz/enigma/ProposingTranslator.java b/src/main/java/cuchaz/enigma/ProposingTranslator.java index 0807880..018fbfd 100644 --- a/src/main/java/cuchaz/enigma/ProposingTranslator.java +++ b/src/main/java/cuchaz/enigma/ProposingTranslator.java @@ -3,41 +3,51 @@ package cuchaz.enigma; import cuchaz.enigma.api.service.NameProposalService; import cuchaz.enigma.translation.Translatable; import cuchaz.enigma.translation.Translator; -import cuchaz.enigma.translation.mapping.*; +import cuchaz.enigma.translation.mapping.EntryRemapper; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; import cuchaz.enigma.translation.representation.entry.Entry; +import java.util.Arrays; import java.util.Optional; public class ProposingTranslator implements Translator { - private final EntryRemapper mapper; - private final NameProposalService nameProposalService; - - public ProposingTranslator(EntryRemapper mapper, NameProposalService nameProposalService) { - this.mapper = mapper; - this.nameProposalService = nameProposalService; - } - - @Override - @SuppressWarnings("unchecked") - public T translate(T translatable) { - if (translatable == null) { - return null; - } - - T deobfuscated = mapper.deobfuscate(translatable); - - if (translatable instanceof Entry && ((Entry) deobfuscated).getName().equals(((Entry) translatable).getName())) { - return mapper.getObfResolver() - .resolveEntry((Entry) translatable, ResolutionStrategy.RESOLVE_ROOT) - .stream() - .map(e1 -> nameProposalService.proposeName(e1, mapper)) - .filter(Optional::isPresent) - .map(Optional::get) - .findFirst() - .map(newName -> (T) ((Entry) deobfuscated).withName(newName)) - .orElse(deobfuscated); - } - - return deobfuscated; - } + private final EntryRemapper mapper; + private final NameProposalService[] nameProposalServices; + + public ProposingTranslator(EntryRemapper mapper, NameProposalService[] nameProposalServices) { + this.mapper = mapper; + this.nameProposalServices = nameProposalServices; + } + + @Override + @SuppressWarnings("unchecked") + public T translate(T translatable) { + if (translatable == null) { + return null; + } + + T deobfuscated = mapper.deobfuscate(translatable); + + if (translatable instanceof Entry && ((Entry) deobfuscated).getName().equals(((Entry) translatable).getName())) { + return mapper.getObfResolver() + .resolveEntry((Entry) translatable, ResolutionStrategy.RESOLVE_ROOT) + .stream() + .map(this::proposeName) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(newName -> (T) ((Entry) deobfuscated).withName(newName)) + .orElse(deobfuscated); + } + + return deobfuscated; + } + + private Optional proposeName(Entry entry) { + return Arrays.stream(nameProposalServices) + .map(service -> service.proposeName(entry, mapper)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } } diff --git a/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java new file mode 100644 index 0000000..12ef709 --- /dev/null +++ b/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java @@ -0,0 +1,150 @@ +package cuchaz.enigma.analysis; + +import com.strobel.core.Pair; +import cuchaz.enigma.api.EnigmaPlugin; +import cuchaz.enigma.api.EnigmaPluginContext; +import cuchaz.enigma.api.service.JarIndexerService; +import cuchaz.enigma.api.service.NameProposalService; +import cuchaz.enigma.translation.mapping.ResolutionStrategy; +import cuchaz.enigma.translation.representation.TypeDescriptor; +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 org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.UnaryOperator; + +public final class BuiltinPlugin implements EnigmaPlugin { + + public BuiltinPlugin() { + } + + @Override + public void init(EnigmaPluginContext ctx) { + registerEnumNamingService(ctx); + } + + private void registerEnumNamingService(EnigmaPluginContext ctx) { + final Map, String> names = new HashMap<>(); + final EnumFieldNameFindingVisitor visitor = new EnumFieldNameFindingVisitor(names); + + ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, ctx1 -> (classCache, jarIndex) -> classCache.visit(() -> visitor, ClassReader.SKIP_FRAMES)); + ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, ctx1 -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry))); + } + + private static final class EnumFieldNameFindingVisitor extends ClassVisitor { + + private ClassEntry clazz; + private String className; + private final Map, String> mappings; + private final Set> enumFields = new HashSet<>(); + private final List classInits = new ArrayList<>(); + + EnumFieldNameFindingVisitor(Map, String> mappings) { + super(Opcodes.ASM7); + this.mappings = mappings; + } + + @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.className = name; + this.clazz = new ClassEntry(name); + this.enumFields.clear(); + this.classInits.clear(); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + if ((access & Opcodes.ACC_ENUM) != 0) { + if (!enumFields.add(new Pair<>(name, descriptor))) { + throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!"); + } + } + return super.visitField(access, name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if ("".equals(name)) { + MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions); + classInits.add(node); + return node; + } + return super.visitMethod(access, name, descriptor, signature, exceptions); + } + + @Override + public void visitEnd() { + super.visitEnd(); + try { + collectResults(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private void collectResults() throws Exception { + String owner = className; + Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); + + for (MethodNode mn : classInits) { + Frame[] frames = analyzer.analyze(className, mn); + + InsnList instrs = mn.instructions; + for (int i = 1; i < instrs.size(); i++) { + AbstractInsnNode instr1 = instrs.get(i - 1); + AbstractInsnNode instr2 = instrs.get(i); + String s = null; + + if (instr2.getOpcode() == Opcodes.PUTSTATIC + && ((FieldInsnNode) instr2).owner.equals(owner) + && enumFields.contains(new Pair<>(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) + && instr1.getOpcode() == Opcodes.INVOKESPECIAL + && "".equals(((MethodInsnNode) instr1).name)) { + + for (int j = 0; j < frames[i - 1].getStackSize(); j++) { + SourceValue sv = frames[i - 1].getStack(j); + for (AbstractInsnNode ci : sv.insns) { + if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) { + //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) { + if (s == null) { + s = (String) (((LdcInsnNode) ci).cst); + // stringsFound++; + } + } + } + } + } + + if (s != null) { + mappings.put(new FieldEntry(clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s); + } + // report otherwise? + } + } + } + } +} diff --git a/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java b/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java index 44f70f8..c1b163d 100644 --- a/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java +++ b/src/main/java/cuchaz/enigma/gui/DecompiledClassSource.java @@ -80,19 +80,15 @@ public class DecompiledClassSource { private Optional proposeName(EnigmaProject project, Entry entry) { EnigmaServices services = project.getEnigma().getServices(); - return services.get(NameProposalService.TYPE).flatMap(nameProposalService -> { - EntryResolver resolver = project.getMapper().getObfResolver(); - - Collection> resolved = resolver.resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); + return services.get(NameProposalService.TYPE).stream().flatMap(nameProposalService -> { EntryRemapper mapper = project.getMapper(); + Collection> resolved = mapper.getObfResolver().resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT); - Stream proposals = resolved.stream() + return resolved.stream() .map(e -> nameProposalService.proposeName(e, mapper)) .filter(Optional::isPresent) .map(Optional::get); - - return proposals.findFirst(); - }); + }).findFirst(); } @Nullable diff --git a/src/main/java/cuchaz/enigma/gui/Gui.java b/src/main/java/cuchaz/enigma/gui/Gui.java index 52a97bb..3ab1cee 100644 --- a/src/main/java/cuchaz/enigma/gui/Gui.java +++ b/src/main/java/cuchaz/enigma/gui/Gui.java @@ -526,6 +526,8 @@ public class Gui { } public void onCaretMove(int pos, boolean fromClick) { + if (controller.project == null) + return; EntryRemapper mapper = controller.project.getMapper(); Token token = this.controller.getToken(pos); boolean isToken = token != null; diff --git a/src/main/java/cuchaz/enigma/gui/GuiController.java b/src/main/java/cuchaz/enigma/gui/GuiController.java index 89fbd10..54b5c92 100644 --- a/src/main/java/cuchaz/enigma/gui/GuiController.java +++ b/src/main/java/cuchaz/enigma/gui/GuiController.java @@ -342,11 +342,11 @@ public class GuiController { visibleClasses.forEach(entry -> { ClassEntry deobfEntry = mapper.deobfuscate(entry); - Optional obfService = enigma.getServices().get(ObfuscationTestService.TYPE); + List obfService = enigma.getServices().get(ObfuscationTestService.TYPE); boolean obfuscated = deobfEntry.equals(entry); - if (obfuscated && obfService.isPresent()) { - if (obfService.get().testDeobfuscated(entry)) { + if (obfuscated && !obfService.isEmpty()) { + if (obfService.stream().anyMatch(service -> service.testDeobfuscated(entry))) { obfuscated = false; } } -- cgit v1.2.3