diff options
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 315b5f63..01967bb6 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 | ||
| 14 | import java.io.IOException; | 14 | import java.io.IOException; |
| 15 | import java.nio.file.Path; | 15 | import java.nio.file.Path; |
| 16 | import java.util.Arrays; | ||
| 17 | import java.util.HashMap; | ||
| 18 | import java.util.HashSet; | ||
| 19 | import java.util.LinkedHashMap; | ||
| 16 | import java.util.List; | 20 | import java.util.List; |
| 21 | import java.util.Map; | ||
| 17 | import java.util.ServiceLoader; | 22 | import java.util.ServiceLoader; |
| 18 | import java.util.Set; | 23 | import java.util.Set; |
| 19 | 24 | ||
| @@ -24,6 +29,7 @@ import org.objectweb.asm.Opcodes; | |||
| 24 | import cuchaz.enigma.analysis.index.JarIndex; | 29 | import cuchaz.enigma.analysis.index.JarIndex; |
| 25 | import cuchaz.enigma.api.EnigmaPlugin; | 30 | import cuchaz.enigma.api.EnigmaPlugin; |
| 26 | import cuchaz.enigma.api.EnigmaPluginContext; | 31 | import cuchaz.enigma.api.EnigmaPluginContext; |
| 32 | import cuchaz.enigma.api.Ordering; | ||
| 27 | import cuchaz.enigma.api.service.EnigmaService; | 33 | import cuchaz.enigma.api.service.EnigmaService; |
| 28 | import cuchaz.enigma.api.service.EnigmaServiceFactory; | 34 | import cuchaz.enigma.api.service.EnigmaServiceFactory; |
| 29 | import cuchaz.enigma.api.service.EnigmaServiceType; | 35 | import cuchaz.enigma.api.service.EnigmaServiceType; |
| @@ -32,6 +38,7 @@ import cuchaz.enigma.classprovider.CachingClassProvider; | |||
| 32 | import cuchaz.enigma.classprovider.ClassProvider; | 38 | import cuchaz.enigma.classprovider.ClassProvider; |
| 33 | import cuchaz.enigma.classprovider.CombiningClassProvider; | 39 | import cuchaz.enigma.classprovider.CombiningClassProvider; |
| 34 | import cuchaz.enigma.classprovider.JarClassProvider; | 40 | import cuchaz.enigma.classprovider.JarClassProvider; |
| 41 | import cuchaz.enigma.utils.OrderingImpl; | ||
| 35 | import cuchaz.enigma.utils.Utils; | 42 | import cuchaz.enigma.utils.Utils; |
| 36 | 43 | ||
| 37 | public class Enigma { | 44 | public 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 f95bf1e3..1bcbaa95 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 | ||
| 3 | import java.io.BufferedReader; | 3 | import java.io.BufferedReader; |
| 4 | import java.io.IOException; | 4 | import java.io.IOException; |
| 5 | import java.io.InputStreamReader; | ||
| 6 | import java.io.Reader; | 5 | import java.io.Reader; |
| 7 | import java.lang.reflect.Type; | ||
| 8 | import java.nio.charset.StandardCharsets; | ||
| 9 | import java.nio.file.Files; | 6 | import java.nio.file.Files; |
| 10 | import java.nio.file.Path; | 7 | import java.nio.file.Path; |
| 11 | import java.util.Collections; | 8 | import java.util.Set; |
| 12 | import java.util.List; | ||
| 13 | import java.util.Map; | ||
| 14 | import java.util.Optional; | ||
| 15 | 9 | ||
| 16 | import javax.annotation.Nullable; | 10 | import javax.annotation.Nullable; |
| 17 | 11 | ||
| 18 | import com.google.common.collect.ImmutableMap; | ||
| 19 | import com.google.gson.Gson; | 12 | import com.google.gson.Gson; |
| 20 | import com.google.gson.GsonBuilder; | ||
| 21 | import com.google.gson.JsonDeserializationContext; | ||
| 22 | import com.google.gson.JsonDeserializer; | ||
| 23 | import com.google.gson.JsonElement; | ||
| 24 | import com.google.gson.JsonObject; | ||
| 25 | import com.google.gson.JsonParseException; | ||
| 26 | import com.google.gson.annotations.SerializedName; | 13 | import com.google.gson.annotations.SerializedName; |
| 27 | import com.google.gson.reflect.TypeToken; | ||
| 28 | 14 | ||
| 29 | import cuchaz.enigma.api.service.EnigmaServiceType; | ||
| 30 | import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; | 15 | import cuchaz.enigma.translation.mapping.serde.MappingFileNameFormat; |
| 31 | import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; | 16 | import cuchaz.enigma.translation.mapping.serde.MappingSaveParameters; |
| 32 | 17 | ||
| 33 | public final class EnigmaProfile { | 18 | public 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 00000000..90029d15 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinNameProposalPlugin.java | |||
| @@ -0,0 +1,143 @@ | |||
| 1 | package cuchaz.enigma.analysis; | ||
| 2 | |||
| 3 | import java.util.ArrayList; | ||
| 4 | import java.util.HashMap; | ||
| 5 | import java.util.HashSet; | ||
| 6 | import java.util.List; | ||
| 7 | import java.util.Map; | ||
| 8 | import java.util.Optional; | ||
| 9 | import java.util.Set; | ||
| 10 | |||
| 11 | import org.objectweb.asm.ClassVisitor; | ||
| 12 | import org.objectweb.asm.FieldVisitor; | ||
| 13 | import org.objectweb.asm.MethodVisitor; | ||
| 14 | import org.objectweb.asm.Opcodes; | ||
| 15 | import org.objectweb.asm.tree.AbstractInsnNode; | ||
| 16 | import org.objectweb.asm.tree.FieldInsnNode; | ||
| 17 | import org.objectweb.asm.tree.InsnList; | ||
| 18 | import org.objectweb.asm.tree.LdcInsnNode; | ||
| 19 | import org.objectweb.asm.tree.MethodInsnNode; | ||
| 20 | import org.objectweb.asm.tree.MethodNode; | ||
| 21 | import org.objectweb.asm.tree.analysis.Analyzer; | ||
| 22 | import org.objectweb.asm.tree.analysis.Frame; | ||
| 23 | import org.objectweb.asm.tree.analysis.SourceInterpreter; | ||
| 24 | import org.objectweb.asm.tree.analysis.SourceValue; | ||
| 25 | |||
| 26 | import cuchaz.enigma.Enigma; | ||
| 27 | import cuchaz.enigma.api.EnigmaPlugin; | ||
| 28 | import cuchaz.enigma.api.EnigmaPluginContext; | ||
| 29 | import cuchaz.enigma.api.service.JarIndexerService; | ||
| 30 | import cuchaz.enigma.api.service.NameProposalService; | ||
| 31 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 32 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 33 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 34 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 35 | import cuchaz.enigma.utils.Pair; | ||
| 36 | |||
| 37 | public 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 0f53d263..aed949af 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 @@ | |||
| 1 | package cuchaz.enigma.analysis; | 1 | package cuchaz.enigma.analysis; |
| 2 | 2 | ||
| 3 | import java.util.ArrayList; | ||
| 4 | import java.util.HashMap; | ||
| 5 | import java.util.HashSet; | ||
| 6 | import java.util.List; | ||
| 7 | import java.util.Map; | ||
| 8 | import java.util.Optional; | ||
| 9 | import java.util.Set; | ||
| 10 | |||
| 11 | import org.objectweb.asm.ClassVisitor; | ||
| 12 | import org.objectweb.asm.FieldVisitor; | ||
| 13 | import org.objectweb.asm.MethodVisitor; | ||
| 14 | import org.objectweb.asm.Opcodes; | ||
| 15 | import org.objectweb.asm.tree.AbstractInsnNode; | ||
| 16 | import org.objectweb.asm.tree.FieldInsnNode; | ||
| 17 | import org.objectweb.asm.tree.InsnList; | ||
| 18 | import org.objectweb.asm.tree.LdcInsnNode; | ||
| 19 | import org.objectweb.asm.tree.MethodInsnNode; | ||
| 20 | import org.objectweb.asm.tree.MethodNode; | ||
| 21 | import org.objectweb.asm.tree.analysis.Analyzer; | ||
| 22 | import org.objectweb.asm.tree.analysis.Frame; | ||
| 23 | import org.objectweb.asm.tree.analysis.SourceInterpreter; | ||
| 24 | import org.objectweb.asm.tree.analysis.SourceValue; | ||
| 25 | |||
| 26 | import cuchaz.enigma.Enigma; | ||
| 27 | import cuchaz.enigma.api.EnigmaPlugin; | 3 | import cuchaz.enigma.api.EnigmaPlugin; |
| 28 | import cuchaz.enigma.api.EnigmaPluginContext; | 4 | import cuchaz.enigma.api.EnigmaPluginContext; |
| 29 | import cuchaz.enigma.api.service.JarIndexerService; | ||
| 30 | import cuchaz.enigma.api.service.NameProposalService; | ||
| 31 | import cuchaz.enigma.source.DecompilerService; | 5 | import cuchaz.enigma.source.DecompilerService; |
| 32 | import cuchaz.enigma.source.Decompilers; | 6 | import cuchaz.enigma.source.Decompilers; |
| 33 | import cuchaz.enigma.translation.representation.TypeDescriptor; | ||
| 34 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 35 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 36 | import cuchaz.enigma.translation.representation.entry.FieldEntry; | ||
| 37 | import cuchaz.enigma.utils.Pair; | ||
| 38 | 7 | ||
| 39 | public final class BuiltinPlugin implements EnigmaPlugin { | 8 | public 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 a59051ad..13713789 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 @@ | |||
| 1 | package cuchaz.enigma.api; | 1 | package cuchaz.enigma.api; |
| 2 | 2 | ||
| 3 | import org.jetbrains.annotations.ApiStatus; | ||
| 4 | |||
| 3 | import cuchaz.enigma.api.service.EnigmaService; | 5 | import cuchaz.enigma.api.service.EnigmaService; |
| 4 | import cuchaz.enigma.api.service.EnigmaServiceFactory; | 6 | import cuchaz.enigma.api.service.EnigmaServiceFactory; |
| 5 | import cuchaz.enigma.api.service.EnigmaServiceType; | 7 | import cuchaz.enigma.api.service.EnigmaServiceType; |
| 6 | 8 | ||
| 9 | @ApiStatus.NonExtendable | ||
| 7 | public interface EnigmaPluginContext { | 10 | public 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 00000000..9a384ba3 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/api/Ordering.java | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | package cuchaz.enigma.api; | ||
| 2 | |||
| 3 | import org.jetbrains.annotations.ApiStatus; | ||
| 4 | |||
| 5 | import cuchaz.enigma.utils.OrderingImpl; | ||
| 6 | |||
| 7 | @ApiStatus.NonExtendable | ||
| 8 | public 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 9e433fb0..00000000 --- a/enigma/src/main/java/cuchaz/enigma/api/service/EnigmaServiceContext.java +++ /dev/null | |||
| @@ -1,11 +0,0 @@ | |||
| 1 | package cuchaz.enigma.api.service; | ||
| 2 | |||
| 3 | import java.util.Optional; | ||
| 4 | |||
| 5 | public 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 7c10ac26..e33ad495 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 @@ | |||
| 1 | package cuchaz.enigma.api.service; | 1 | package cuchaz.enigma.api.service; |
| 2 | 2 | ||
| 3 | public interface EnigmaServiceFactory<T extends EnigmaService> { | 3 | public 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 00000000..c68ccb79 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/OrderingImpl.java | |||
| @@ -0,0 +1,129 @@ | |||
| 1 | package cuchaz.enigma.utils; | ||
| 2 | |||
| 3 | import java.util.ArrayDeque; | ||
| 4 | import java.util.ArrayList; | ||
| 5 | import java.util.Deque; | ||
| 6 | import java.util.HashMap; | ||
| 7 | import java.util.LinkedHashSet; | ||
| 8 | import java.util.List; | ||
| 9 | import java.util.Map; | ||
| 10 | import java.util.Set; | ||
| 11 | |||
| 12 | import cuchaz.enigma.api.Ordering; | ||
| 13 | |||
| 14 | public 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 136a3e78..ab979a8c 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 @@ | |||
| 1 | cuchaz.enigma.analysis.BuiltinPlugin | 1 | cuchaz.enigma.analysis.BuiltinPlugin |
| 2 | cuchaz.enigma.analysis.BuiltinNameProposalPlugin | ||
diff --git a/enigma/src/main/resources/profile.json b/enigma/src/main/resources/profile.json deleted file mode 100644 index e1af4cdb..00000000 --- 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 | ||