summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Joseph Burton2025-08-20 18:15:59 +0100
committerGravatar GitHub2025-08-20 18:15:59 +0100
commit9abdbd5352115191d8ad5bb7018a5497430aa8db (patch)
treebc1e94edc0a207951f0838361a70140c57902061
parentSupport multiple input jars (#553) (diff)
downloadenigma-fork-9abdbd5352115191d8ad5bb7018a5497430aa8db.tar.gz
enigma-fork-9abdbd5352115191d8ad5bb7018a5497430aa8db.tar.xz
enigma-fork-9abdbd5352115191d8ad5bb7018a5497430aa8db.zip
Services rework (#554)
* Refactor services * Split out built-in name proposal into a separate plugin so that it can be disabled separately * Fix checkstyle
-rw-r--r--enigma/src/main/java/cuchaz/enigma/Enigma.java66
-rw-r--r--enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java100
-rw-r--r--enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java143
-rw-r--r--enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java148
-rw-r--r--enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java6
-rw-r--r--enigma/src/main/java/cuchaz/enigma/api/Ordering.java24
-rw-r--r--enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java11
-rw-r--r--enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java2
-rw-r--r--enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java129
-rw-r--r--enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin1
-rw-r--r--enigma/src/main/resources/profile.json20
11 files changed, 358 insertions, 292 deletions
diff --git a/enigma/src/main/java/cuchaz/enigma/Enigma.java b/enigma/src/main/java/cuchaz/enigma/Enigma.java
index 315b5f6..01967bb 100644
--- a/enigma/src/main/java/cuchaz/enigma/Enigma.java
+++ b/enigma/src/main/java/cuchaz/enigma/Enigma.java
@@ -13,7 +13,12 @@ package cuchaz.enigma;
13 13
14import java.io.IOException; 14import java.io.IOException;
15import java.nio.file.Path; 15import java.nio.file.Path;
16import java.util.Arrays;
17import java.util.HashMap;
18import java.util.HashSet;
19import java.util.LinkedHashMap;
16import java.util.List; 20import java.util.List;
21import java.util.Map;
17import java.util.ServiceLoader; 22import java.util.ServiceLoader;
18import java.util.Set; 23import java.util.Set;
19 24
@@ -24,6 +29,7 @@ import org.objectweb.asm.Opcodes;
24import cuchaz.enigma.analysis.index.JarIndex; 29import cuchaz.enigma.analysis.index.JarIndex;
25import cuchaz.enigma.api.EnigmaPlugin; 30import cuchaz.enigma.api.EnigmaPlugin;
26import cuchaz.enigma.api.EnigmaPluginContext; 31import cuchaz.enigma.api.EnigmaPluginContext;
32import cuchaz.enigma.api.Ordering;
27import cuchaz.enigma.api.service.EnigmaService; 33import cuchaz.enigma.api.service.EnigmaService;
28import cuchaz.enigma.api.service.EnigmaServiceFactory; 34import cuchaz.enigma.api.service.EnigmaServiceFactory;
29import cuchaz.enigma.api.service.EnigmaServiceType; 35import cuchaz.enigma.api.service.EnigmaServiceType;
@@ -32,6 +38,7 @@ import cuchaz.enigma.classprovider.CachingClassProvider;
32import cuchaz.enigma.classprovider.ClassProvider; 38import cuchaz.enigma.classprovider.ClassProvider;
33import cuchaz.enigma.classprovider.CombiningClassProvider; 39import cuchaz.enigma.classprovider.CombiningClassProvider;
34import cuchaz.enigma.classprovider.JarClassProvider; 40import cuchaz.enigma.classprovider.JarClassProvider;
41import cuchaz.enigma.utils.OrderingImpl;
35import cuchaz.enigma.utils.Utils; 42import cuchaz.enigma.utils.Utils;
36 43
37public class Enigma { 44public class Enigma {
@@ -96,7 +103,6 @@ public class Enigma {
96 103
97 public static class Builder { 104 public static class Builder {
98 private EnigmaProfile profile = EnigmaProfile.EMPTY; 105 private EnigmaProfile profile = EnigmaProfile.EMPTY;
99 private Iterable<EnigmaPlugin> plugins = ServiceLoader.load(EnigmaPlugin.class);
100 106
101 private Builder() { 107 private Builder() {
102 } 108 }
@@ -107,18 +113,12 @@ public class Enigma {
107 return this; 113 return this;
108 } 114 }
109 115
110 public Builder setPlugins(Iterable<EnigmaPlugin> plugins) {
111 Preconditions.checkNotNull(plugins, "plugins cannot be null");
112 this.plugins = plugins;
113 return this;
114 }
115
116 public Enigma build() { 116 public Enigma build() {
117 PluginContext pluginContext = new PluginContext(profile); 117 PluginContext pluginContext = new PluginContext();
118 118
119 for (EnigmaPlugin plugin : plugins) { 119 ServiceLoader.load(EnigmaPlugin.class).stream()
120 plugin.init(pluginContext); 120 .filter(plugin -> !profile.getDisabledPlugins().contains(plugin.type().getName()))
121 } 121 .forEach(plugin -> plugin.get().init(pluginContext));
122 122
123 EnigmaServices services = pluginContext.buildServices(); 123 EnigmaServices services = pluginContext.buildServices();
124 return new Enigma(profile, services); 124 return new Enigma(profile, services);
@@ -126,30 +126,44 @@ public class Enigma {
126 } 126 }
127 127
128 private static class PluginContext implements EnigmaPluginContext { 128 private static class PluginContext implements EnigmaPluginContext {
129 private final EnigmaProfile profile; 129 private final Map<EnigmaServiceType<?>, PendingServices<?>> pendingServices = new HashMap<>();
130 130
131 private final ImmutableListMultimap.Builder<EnigmaServiceType<?>, EnigmaService> services = ImmutableListMultimap.builder(); 131 @Override
132 132 public <T extends EnigmaService> void registerService(String id, EnigmaServiceType<T> serviceType, EnigmaServiceFactory<T> factory, Ordering... ordering) {
133 PluginContext(EnigmaProfile profile) { 133 @SuppressWarnings("unchecked")
134 this.profile = profile; 134 PendingServices<T> pending = (PendingServices<T>) pendingServices.computeIfAbsent(serviceType, k -> new PendingServices<>());
135 pending.factories.put(id, factory);
136 pending.orderings.put(id, Arrays.asList(ordering));
135 } 137 }
136 138
137 @Override 139 @Override
138 public <T extends EnigmaService> void registerService(String id, EnigmaServiceType<T> serviceType, EnigmaServiceFactory<T> factory) { 140 public void disableService(String id, EnigmaServiceType<?> serviceType) {
139 List<EnigmaProfile.Service> serviceProfiles = profile.getServiceProfiles(serviceType); 141 pendingServices.computeIfAbsent(serviceType, k -> new PendingServices<>()).disabled.add(id);
140
141 for (EnigmaProfile.Service serviceProfile : serviceProfiles) {
142 if (serviceProfile.matches(id)) {
143 T service = factory.create(serviceProfile::getArgument);
144 services.put(serviceType, service);
145 break;
146 }
147 }
148 } 142 }
149 143
150 EnigmaServices buildServices() { 144 EnigmaServices buildServices() {
145 ImmutableListMultimap.Builder<EnigmaServiceType<?>, EnigmaService> services = ImmutableListMultimap.builder();
146
147 pendingServices.forEach((serviceType, pending) -> {
148 pending.orderings.keySet().removeAll(pending.disabled);
149 List<String> orderedServices = OrderingImpl.sort(serviceType.key, pending.orderings);
150 orderedServices.forEach(serviceId -> {
151 services.put(serviceType, pending.factories.get(serviceId).create());
152 });
153 });
154
151 return new EnigmaServices(services.build()); 155 return new EnigmaServices(services.build());
152 } 156 }
157
158 private record PendingServices<T extends EnigmaService>(
159 Map<String, EnigmaServiceFactory<T>> factories,
160 Map<String, List<Ordering>> orderings,
161 Set<String> disabled
162 ) {
163 PendingServices() {
164 this(new HashMap<>(), new LinkedHashMap<>(), new HashSet<>());
165 }
166 }
153 } 167 }
154 168
155 static { 169 static {
diff --git a/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java b/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java
index f95bf1e..1bcbaa9 100644
--- a/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java
+++ b/enigma/src/main/java/cuchaz/enigma/EnigmaProfile.java
@@ -2,50 +2,32 @@ package cuchaz.enigma;
2 2
3import java.io.BufferedReader; 3import java.io.BufferedReader;
4import java.io.IOException; 4import java.io.IOException;
5import java.io.InputStreamReader;
6import java.io.Reader; 5import java.io.Reader;
7import java.lang.reflect.Type;
8import java.nio.charset.StandardCharsets;
9import java.nio.file.Files; 6import java.nio.file.Files;
10import java.nio.file.Path; 7import java.nio.file.Path;
11import java.util.Collections; 8import java.util.Set;
12import java.util.List;
13import java.util.Map;
14import java.util.Optional;
15 9
16import javax.annotation.Nullable; 10import javax.annotation.Nullable;
17 11
18import com.google.common.collect.ImmutableMap;
19import com.google.gson.Gson; 12import com.google.gson.Gson;
20import com.google.gson.GsonBuilder;
21import com.google.gson.JsonDeserializationContext;
22import com.google.gson.JsonDeserializer;
23import com.google.gson.JsonElement;
24import com.google.gson.JsonObject;
25import com.google.gson.JsonParseException;
26import com.google.gson.annotations.SerializedName; 13import com.google.gson.annotations.SerializedName;
27import com.google.gson.reflect.TypeToken;
28 14
29import cuchaz.enigma.api.service.EnigmaServiceType;
30import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; 15import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat;
31import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; 16import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters;
32 17
33public final class EnigmaProfile { 18public final class EnigmaProfile {
34 public static final EnigmaProfile EMPTY = new EnigmaProfile(new ServiceContainer(ImmutableMap.of())); 19 public static final EnigmaProfile EMPTY = new EnigmaProfile();
35 20
36 private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF); 21 private static final MappingSaveParameters DEFAULT_MAPPING_SAVE_PARAMETERS = new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF);
37 private static final Gson GSON = new GsonBuilder().registerTypeAdapter(ServiceContainer.class, (JsonDeserializer<ServiceContainer>) EnigmaProfile::loadServiceContainer).create(); 22 private static final Gson GSON = new Gson();
38 private static final Type SERVICE_LIST_TYPE = new TypeToken<List<Service>>() {
39 }.getType();
40 23
41 @SerializedName("services") 24 @SerializedName("disabled_plugins")
42 private final ServiceContainer serviceProfiles; 25 private final Set<String> disabledPlugins = Set.of();
43 26
44 @SerializedName("mapping_save_parameters") 27 @SerializedName("mapping_save_parameters")
45 private final MappingSaveParameters mappingSaveParameters = null; 28 private final MappingSaveParameters mappingSaveParameters = DEFAULT_MAPPING_SAVE_PARAMETERS;
46 29
47 private EnigmaProfile(ServiceContainer serviceProfiles) { 30 private EnigmaProfile() {
48 this.serviceProfiles = serviceProfiles;
49 } 31 }
50 32
51 public static EnigmaProfile read(@Nullable Path file) throws IOException { 33 public static EnigmaProfile read(@Nullable Path file) throws IOException {
@@ -54,12 +36,7 @@ public final class EnigmaProfile {
54 return EnigmaProfile.parse(reader); 36 return EnigmaProfile.parse(reader);
55 } 37 }
56 } else { 38 } else {
57 try (BufferedReader reader = new BufferedReader(new InputStreamReader(EnigmaProfile.class.getResourceAsStream("/profile.json"), StandardCharsets.UTF_8))) { 39 return EMPTY;
58 return EnigmaProfile.parse(reader);
59 } catch (IOException ex) {
60 System.err.println("Failed to load default profile, will use empty profile: " + ex.getMessage());
61 return EnigmaProfile.EMPTY;
62 }
63 } 40 }
64 } 41 }
65 42
@@ -67,66 +44,11 @@ public final class EnigmaProfile {
67 return GSON.fromJson(reader, EnigmaProfile.class); 44 return GSON.fromJson(reader, EnigmaProfile.class);
68 } 45 }
69 46
70 private static ServiceContainer loadServiceContainer(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 47 public Set<String> getDisabledPlugins() {
71 if (!json.isJsonObject()) { 48 return disabledPlugins;
72 throw new JsonParseException("services must be an Object!");
73 }
74
75 JsonObject object = json.getAsJsonObject();
76
77 ImmutableMap.Builder<String, List<Service>> builder = ImmutableMap.builder();
78
79 for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
80 JsonElement value = entry.getValue();
81
82 if (value.isJsonObject()) {
83 builder.put(entry.getKey(), Collections.singletonList(GSON.fromJson(value, Service.class)));
84 } else if (value.isJsonArray()) {
85 builder.put(entry.getKey(), GSON.fromJson(value, SERVICE_LIST_TYPE));
86 } else {
87 throw new JsonParseException(String.format("Don't know how to convert %s to a list of service!", value));
88 }
89 }
90
91 return new ServiceContainer(builder.build());
92 }
93
94 public List<Service> getServiceProfiles(EnigmaServiceType<?> serviceType) {
95 return serviceProfiles.get(serviceType.key);
96 } 49 }
97 50
98 public MappingSaveParameters getMappingSaveParameters() { 51 public MappingSaveParameters getMappingSaveParameters() {
99 //noinspection ConstantConditions 52 return mappingSaveParameters;
100 return mappingSaveParameters == null ? EnigmaProfile.DEFAULT_MAPPING_SAVE_PARAMETERS : mappingSaveParameters;
101 }
102
103 public static class Service {
104 private final String id;
105 private final Map<String, String> args;
106
107 Service(String id, Map<String, String> args) {
108 this.id = id;
109 this.args = args;
110 }
111
112 public boolean matches(String id) {
113 return this.id.equals(id);
114 }
115
116 public Optional<String> getArgument(String key) {
117 return args != null ? Optional.ofNullable(args.get(key)) : Optional.empty();
118 }
119 }
120
121 static final class ServiceContainer {
122 private final Map<String, List<Service>> services;
123
124 ServiceContainer(Map<String, List<Service>> services) {
125 this.services = services;
126 }
127
128 List<Service> get(String key) {
129 return services.getOrDefault(key, Collections.emptyList());
130 }
131 } 53 }
132} 54}
diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java
new file mode 100644
index 0000000..90029d1
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java
@@ -0,0 +1,143 @@
1package cuchaz.enigma.analysis;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.HashSet;
6import java.util.List;
7import java.util.Map;
8import java.util.Optional;
9import java.util.Set;
10
11import org.objectweb.asm.ClassVisitor;
12import org.objectweb.asm.FieldVisitor;
13import org.objectweb.asm.MethodVisitor;
14import org.objectweb.asm.Opcodes;
15import org.objectweb.asm.tree.AbstractInsnNode;
16import org.objectweb.asm.tree.FieldInsnNode;
17import org.objectweb.asm.tree.InsnList;
18import org.objectweb.asm.tree.LdcInsnNode;
19import org.objectweb.asm.tree.MethodInsnNode;
20import org.objectweb.asm.tree.MethodNode;
21import org.objectweb.asm.tree.analysis.Analyzer;
22import org.objectweb.asm.tree.analysis.Frame;
23import org.objectweb.asm.tree.analysis.SourceInterpreter;
24import org.objectweb.asm.tree.analysis.SourceValue;
25
26import cuchaz.enigma.Enigma;
27import cuchaz.enigma.api.EnigmaPlugin;
28import cuchaz.enigma.api.EnigmaPluginContext;
29import cuchaz.enigma.api.service.JarIndexerService;
30import cuchaz.enigma.api.service.NameProposalService;
31import cuchaz.enigma.translation.representation.TypeDescriptor;
32import cuchaz.enigma.translation.representation.entry.ClassEntry;
33import cuchaz.enigma.translation.representation.entry.Entry;
34import cuchaz.enigma.translation.representation.entry.FieldEntry;
35import cuchaz.enigma.utils.Pair;
36
37public class BuiltinNameProposalPlugin implements EnigmaPlugin {
38 @Override
39 public void init(EnigmaPluginContext ctx) {
40 final Map<Entry<?>, String> names = new HashMap<>();
41 JarIndexerService indexerService = JarIndexerService.fromVisitorsInParallel(EnumFieldNameFindingVisitor::new, visitors -> visitors.forEach(visitor -> names.putAll(visitor.mappings)));
42
43 ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, () -> indexerService);
44 ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, () -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry)));
45 }
46
47 private static final class EnumFieldNameFindingVisitor extends ClassVisitor {
48 private ClassEntry clazz;
49 private String className;
50 private final Map<Entry<?>, String> mappings = new HashMap<>();
51 private final Set<Pair<String, String>> enumFields = new HashSet<>();
52 private final List<MethodNode> classInits = new ArrayList<>();
53
54 EnumFieldNameFindingVisitor() {
55 super(Enigma.ASM_VERSION);
56 }
57
58 @Override
59 public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
60 super.visit(version, access, name, signature, superName, interfaces);
61 this.className = name;
62 this.clazz = new ClassEntry(name);
63 this.enumFields.clear();
64 this.classInits.clear();
65 }
66
67 @Override
68 public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
69 if ((access & Opcodes.ACC_ENUM) != 0) {
70 if (!enumFields.add(new Pair<>(name, descriptor))) {
71 throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!");
72 }
73 }
74
75 return super.visitField(access, name, descriptor, signature, value);
76 }
77
78 @Override
79 public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
80 if ("<clinit>".equals(name)) {
81 MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions);
82 classInits.add(node);
83 return node;
84 }
85
86 return super.visitMethod(access, name, descriptor, signature, exceptions);
87 }
88
89 @Override
90 public void visitEnd() {
91 super.visitEnd();
92
93 try {
94 collectResults();
95 } catch (Exception ex) {
96 throw new RuntimeException(ex);
97 }
98 }
99
100 private void collectResults() throws Exception {
101 if (enumFields.isEmpty()) {
102 return;
103 }
104
105 String owner = className;
106 Analyzer<SourceValue> analyzer = new Analyzer<>(new SourceInterpreter());
107
108 for (MethodNode mn : classInits) {
109 Frame<SourceValue>[] frames = analyzer.analyze(className, mn);
110 InsnList instrs = mn.instructions;
111
112 for (int i = 1; i < instrs.size(); i++) {
113 AbstractInsnNode instr1 = instrs.get(i - 1);
114 AbstractInsnNode instr2 = instrs.get(i);
115 String s = null;
116
117 if (instr2.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) instr2).owner.equals(owner) && enumFields.contains(new Pair<>(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) && instr1.getOpcode() == Opcodes.INVOKESPECIAL && "<init>".equals(
118 ((MethodInsnNode) instr1).name)) {
119 for (int j = 0; j < frames[i - 1].getStackSize(); j++) {
120 SourceValue sv = frames[i - 1].getStack(j);
121
122 for (AbstractInsnNode ci : sv.insns) {
123 if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) {
124 //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) {
125 if (s == null) {
126 s = (String) (((LdcInsnNode) ci).cst);
127 // stringsFound++;
128 }
129 }
130 }
131 }
132 }
133
134 if (s != null) {
135 mappings.put(new FieldEntry(clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s);
136 }
137
138 // report otherwise?
139 }
140 }
141 }
142 }
143}
diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java
index 0f53d26..aed949a 100644
--- a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java
+++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java
@@ -1,160 +1,20 @@
1package cuchaz.enigma.analysis; 1package cuchaz.enigma.analysis;
2 2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.HashSet;
6import java.util.List;
7import java.util.Map;
8import java.util.Optional;
9import java.util.Set;
10
11import org.objectweb.asm.ClassVisitor;
12import org.objectweb.asm.FieldVisitor;
13import org.objectweb.asm.MethodVisitor;
14import org.objectweb.asm.Opcodes;
15import org.objectweb.asm.tree.AbstractInsnNode;
16import org.objectweb.asm.tree.FieldInsnNode;
17import org.objectweb.asm.tree.InsnList;
18import org.objectweb.asm.tree.LdcInsnNode;
19import org.objectweb.asm.tree.MethodInsnNode;
20import org.objectweb.asm.tree.MethodNode;
21import org.objectweb.asm.tree.analysis.Analyzer;
22import org.objectweb.asm.tree.analysis.Frame;
23import org.objectweb.asm.tree.analysis.SourceInterpreter;
24import org.objectweb.asm.tree.analysis.SourceValue;
25
26import cuchaz.enigma.Enigma;
27import cuchaz.enigma.api.EnigmaPlugin; 3import cuchaz.enigma.api.EnigmaPlugin;
28import cuchaz.enigma.api.EnigmaPluginContext; 4import cuchaz.enigma.api.EnigmaPluginContext;
29import cuchaz.enigma.api.service.JarIndexerService;
30import cuchaz.enigma.api.service.NameProposalService;
31import cuchaz.enigma.source.DecompilerService; 5import cuchaz.enigma.source.DecompilerService;
32import cuchaz.enigma.source.Decompilers; 6import cuchaz.enigma.source.Decompilers;
33import cuchaz.enigma.translation.representation.TypeDescriptor;
34import cuchaz.enigma.translation.representation.entry.ClassEntry;
35import cuchaz.enigma.translation.representation.entry.Entry;
36import cuchaz.enigma.translation.representation.entry.FieldEntry;
37import cuchaz.enigma.utils.Pair;
38 7
39public final class BuiltinPlugin implements EnigmaPlugin { 8public final class BuiltinPlugin implements EnigmaPlugin {
40 public BuiltinPlugin() {
41 }
42
43 @Override 9 @Override
44 public void init(EnigmaPluginContext ctx) { 10 public void init(EnigmaPluginContext ctx) {
45 registerEnumNamingService(ctx);
46 registerDecompilerServices(ctx); 11 registerDecompilerServices(ctx);
47 } 12 }
48 13
49 private void registerEnumNamingService(EnigmaPluginContext ctx) {
50 final Map<Entry<?>, String> names = new HashMap<>();
51 JarIndexerService indexerService = JarIndexerService.fromVisitorsInParallel(EnumFieldNameFindingVisitor::new, visitors -> visitors.forEach(visitor -> names.putAll(visitor.mappings)));
52
53 ctx.registerService("enigma:enum_initializer_indexer", JarIndexerService.TYPE, ctx1 -> indexerService);
54 ctx.registerService("enigma:enum_name_proposer", NameProposalService.TYPE, ctx1 -> (obfEntry, remapper) -> Optional.ofNullable(names.get(obfEntry)));
55 }
56
57 private void registerDecompilerServices(EnigmaPluginContext ctx) { 14 private void registerDecompilerServices(EnigmaPluginContext ctx) {
58 ctx.registerService("enigma:vineflower", DecompilerService.TYPE, ctx1 -> Decompilers.VINEFLOWER); 15 ctx.registerService("enigma:vineflower", DecompilerService.TYPE, () -> Decompilers.VINEFLOWER);
59 ctx.registerService("enigma:cfr", DecompilerService.TYPE, ctx1 -> Decompilers.CFR); 16 ctx.registerService("enigma:cfr", DecompilerService.TYPE, () -> Decompilers.CFR);
60 ctx.registerService("enigma:procyon", DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON); 17 ctx.registerService("enigma:procyon", DecompilerService.TYPE, () -> Decompilers.PROCYON);
61 ctx.registerService("enigma:bytecode", DecompilerService.TYPE, ctx1 -> Decompilers.BYTECODE); 18 ctx.registerService("enigma:bytecode", DecompilerService.TYPE, () -> Decompilers.BYTECODE);
62 }
63
64 private static final class EnumFieldNameFindingVisitor extends ClassVisitor {
65 private ClassEntry clazz;
66 private String className;
67 private final Map<Entry<?>, String> mappings = new HashMap<>();
68 private final Set<Pair<String, String>> enumFields = new HashSet<>();
69 private final List<MethodNode> classInits = new ArrayList<>();
70
71 EnumFieldNameFindingVisitor() {
72 super(Enigma.ASM_VERSION);
73 }
74
75 @Override
76 public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
77 super.visit(version, access, name, signature, superName, interfaces);
78 this.className = name;
79 this.clazz = new ClassEntry(name);
80 this.enumFields.clear();
81 this.classInits.clear();
82 }
83
84 @Override
85 public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
86 if ((access & Opcodes.ACC_ENUM) != 0) {
87 if (!enumFields.add(new Pair<>(name, descriptor))) {
88 throw new IllegalArgumentException("Found two enum fields with the same name \"" + name + "\" and desc \"" + descriptor + "\"!");
89 }
90 }
91
92 return super.visitField(access, name, descriptor, signature, value);
93 }
94
95 @Override
96 public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
97 if ("<clinit>".equals(name)) {
98 MethodNode node = new MethodNode(api, access, name, descriptor, signature, exceptions);
99 classInits.add(node);
100 return node;
101 }
102
103 return super.visitMethod(access, name, descriptor, signature, exceptions);
104 }
105
106 @Override
107 public void visitEnd() {
108 super.visitEnd();
109
110 try {
111 collectResults();
112 } catch (Exception ex) {
113 throw new RuntimeException(ex);
114 }
115 }
116
117 private void collectResults() throws Exception {
118 if (enumFields.isEmpty()) {
119 return;
120 }
121
122 String owner = className;
123 Analyzer<SourceValue> analyzer = new Analyzer<>(new SourceInterpreter());
124
125 for (MethodNode mn : classInits) {
126 Frame<SourceValue>[] frames = analyzer.analyze(className, mn);
127 InsnList instrs = mn.instructions;
128
129 for (int i = 1; i < instrs.size(); i++) {
130 AbstractInsnNode instr1 = instrs.get(i - 1);
131 AbstractInsnNode instr2 = instrs.get(i);
132 String s = null;
133
134 if (instr2.getOpcode() == Opcodes.PUTSTATIC && ((FieldInsnNode) instr2).owner.equals(owner) && enumFields.contains(new Pair<>(((FieldInsnNode) instr2).name, ((FieldInsnNode) instr2).desc)) && instr1.getOpcode() == Opcodes.INVOKESPECIAL && "<init>".equals(
135 ((MethodInsnNode) instr1).name)) {
136 for (int j = 0; j < frames[i - 1].getStackSize(); j++) {
137 SourceValue sv = frames[i - 1].getStack(j);
138
139 for (AbstractInsnNode ci : sv.insns) {
140 if (ci instanceof LdcInsnNode && ((LdcInsnNode) ci).cst instanceof String) {
141 //if (s == null || !s.equals(((LdcInsnNode) ci).cst)) {
142 if (s == null) {
143 s = (String) (((LdcInsnNode) ci).cst);
144 // stringsFound++;
145 }
146 }
147 }
148 }
149 }
150
151 if (s != null) {
152 mappings.put(new FieldEntry(clazz, ((FieldInsnNode) instr2).name, new TypeDescriptor(((FieldInsnNode) instr2).desc)), s);
153 }
154
155 // report otherwise?
156 }
157 }
158 }
159 } 19 }
160} 20}
diff --git a/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java
index a59051a..1371378 100644
--- a/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java
+++ b/enigma/src/main/java/cuchaz/enigma/api/EnigmaPluginContext.java
@@ -1,9 +1,13 @@
1package cuchaz.enigma.api; 1package cuchaz.enigma.api;
2 2
3import org.jetbrains.annotations.ApiStatus;
4
3import cuchaz.enigma.api.service.EnigmaService; 5import cuchaz.enigma.api.service.EnigmaService;
4import cuchaz.enigma.api.service.EnigmaServiceFactory; 6import cuchaz.enigma.api.service.EnigmaServiceFactory;
5import cuchaz.enigma.api.service.EnigmaServiceType; 7import cuchaz.enigma.api.service.EnigmaServiceType;
6 8
9@ApiStatus.NonExtendable
7public interface EnigmaPluginContext { 10public interface EnigmaPluginContext {
8 <T extends EnigmaService> void registerService(String id, EnigmaServiceType<T> serviceType, EnigmaServiceFactory<T> factory); 11 <T extends EnigmaService> void registerService(String id, EnigmaServiceType<T> serviceType, EnigmaServiceFactory<T> factory, Ordering... ordering);
12 void disableService(String id, EnigmaServiceType<?> serviceType);
9} 13}
diff --git a/enigma/src/main/java/cuchaz/enigma/api/Ordering.java b/enigma/src/main/java/cuchaz/enigma/api/Ordering.java
new file mode 100644
index 0000000..9a384ba
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/api/Ordering.java
@@ -0,0 +1,24 @@
1package cuchaz.enigma.api;
2
3import org.jetbrains.annotations.ApiStatus;
4
5import cuchaz.enigma.utils.OrderingImpl;
6
7@ApiStatus.NonExtendable
8public interface Ordering {
9 static Ordering first() {
10 return OrderingImpl.First.INSTANCE;
11 }
12
13 static Ordering last() {
14 return OrderingImpl.Last.INSTANCE;
15 }
16
17 static Ordering before(String id) {
18 return new OrderingImpl.Before(id);
19 }
20
21 static Ordering after(String id) {
22 return new OrderingImpl.After(id);
23 }
24}
diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java
deleted file mode 100644
index 9e433fb..0000000
--- a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java
+++ /dev/null
@@ -1,11 +0,0 @@
1package cuchaz.enigma.api.service;
2
3import java.util.Optional;
4
5public interface EnigmaServiceContext<T extends EnigmaService> {
6 static <T extends EnigmaService> EnigmaServiceContext<T> empty() {
7 return key -> Optional.empty();
8 }
9
10 Optional<String> getArgument(String key);
11}
diff --git a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java
index 7c10ac2..e33ad49 100644
--- a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java
+++ b/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceFactory.java
@@ -1,5 +1,5 @@
1package cuchaz.enigma.api.service; 1package cuchaz.enigma.api.service;
2 2
3public interface EnigmaServiceFactory<T extends EnigmaService> { 3public interface EnigmaServiceFactory<T extends EnigmaService> {
4 T create(EnigmaServiceContext<T> ctx); 4 T create();
5} 5}
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java b/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java
new file mode 100644
index 0000000..c68ccb7
--- /dev/null
+++ b/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java
@@ -0,0 +1,129 @@
1package cuchaz.enigma.utils;
2
3import java.util.ArrayDeque;
4import java.util.ArrayList;
5import java.util.Deque;
6import java.util.HashMap;
7import java.util.LinkedHashSet;
8import java.util.List;
9import java.util.Map;
10import java.util.Set;
11
12import cuchaz.enigma.api.Ordering;
13
14public sealed interface OrderingImpl extends Ordering {
15 static List<String> sort(String serviceName, Map<String, List<Ordering>> idToOrderings) {
16 List<String> first = new ArrayList<>();
17 List<String> middle = new ArrayList<>();
18 Map<String, Integer> inDegree = new HashMap<>();
19 Map<String, Set<String>> graph = new HashMap<>();
20
21 idToOrderings.forEach((id, orderings) -> {
22 inDegree.put(id, 0);
23 graph.put(id, new LinkedHashSet<>());
24
25 boolean isFirst = false;
26 boolean isLast = false;
27
28 for (Ordering ordering : orderings) {
29 if (ordering instanceof First) {
30 isFirst = true;
31 } else if (ordering instanceof Last) {
32 isLast = true;
33 }
34 }
35
36 if (isFirst) {
37 if (isLast) {
38 throw new IllegalArgumentException("Service " + id + " has both 'first' and 'last' ordering");
39 }
40
41 first.add(id);
42 } else if (!isLast) {
43 middle.add(id);
44 }
45 });
46
47 idToOrderings.forEach((id, orderings) -> {
48 boolean isFirst = false;
49 boolean isLast = false;
50
51 for (Ordering ordering : orderings) {
52 if (ordering instanceof Before before) {
53 if (idToOrderings.containsKey(before.id())) {
54 if (graph.get(id).add(before.id())) {
55 inDegree.merge(before.id(), 1, Integer::sum);
56 }
57 }
58 } else if (ordering instanceof After after) {
59 if (idToOrderings.containsKey(after.id())) {
60 if (graph.get(after.id()).add(id)) {
61 inDegree.merge(id, 1, Integer::sum);
62 }
63 }
64 } else if (ordering instanceof First) {
65 isFirst = true;
66 } else if (ordering instanceof Last) {
67 isLast = true;
68 }
69 }
70
71 if (!isFirst) {
72 for (String aFirst : first) {
73 if (graph.get(aFirst).add(id)) {
74 inDegree.merge(id, 1, Integer::sum);
75 }
76 }
77
78 if (isLast) {
79 for (String aMiddle : middle) {
80 if (graph.get(aMiddle).add(id)) {
81 inDegree.merge(id, 1, Integer::sum);
82 }
83 }
84 }
85 }
86 });
87
88 Deque<String> queue = new ArrayDeque<>();
89
90 for (String id : idToOrderings.keySet()) {
91 if (inDegree.get(id) == 0) {
92 queue.add(id);
93 }
94 }
95
96 List<String> result = new ArrayList<>(idToOrderings.size());
97
98 while (!queue.isEmpty()) {
99 String id = queue.remove();
100 result.add(id);
101
102 for (String successor : graph.get(id)) {
103 if (inDegree.merge(successor, -1, Integer::sum) == 0) {
104 queue.add(successor);
105 }
106 }
107 }
108
109 if (result.size() != idToOrderings.size()) {
110 throw new IllegalStateException("Services in " + serviceName + " contain circular dependencies");
111 }
112
113 return result;
114 }
115
116 enum First implements OrderingImpl {
117 INSTANCE
118 }
119
120 enum Last implements OrderingImpl {
121 INSTANCE
122 }
123
124 record Before(String id) implements OrderingImpl {
125 }
126
127 record After(String id) implements OrderingImpl {
128 }
129}
diff --git a/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin b/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin
index 136a3e7..ab979a8 100644
--- a/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin
+++ b/enigma/src/main/resources/META-INF/services/cuchaz.enigma.api.EnigmaPlugin
@@ -1 +1,2 @@
1cuchaz.enigma.analysis.BuiltinPlugin 1cuchaz.enigma.analysis.BuiltinPlugin
2cuchaz.enigma.analysis.BuiltinNameProposalPlugin
diff --git a/enigma/src/main/resources/profile.json b/enigma/src/main/resources/profile.json
deleted file mode 100644
index e1af4cd..0000000
--- a/enigma/src/main/resources/profile.json
+++ /dev/null
@@ -1,20 +0,0 @@
1{
2 "services": {
3 "jar_indexer": [
4 {
5 "id": "enigma:enum_initializer_indexer"
6 },
7 {
8 "id": "enigma:specialized_bridge_method_indexer"
9 }
10 ],
11 "name_proposal": [
12 {
13 "id": "enigma:enum_name_proposer"
14 },
15 {
16 "id": "enigma:specialized_method_name_proposer"
17 }
18 ]
19 }
20} \ No newline at end of file