fix: improve plugins data handling

This commit is contained in:
Skylot
2023-04-01 21:06:05 +01:00
parent a992c93198
commit 7a309ca367
35 changed files with 786 additions and 562 deletions
@@ -138,7 +138,7 @@ public class JadxWrapper {
private void initGuiPluginsContext() {
guiPluginsContext = new GuiPluginsContext(mainWindow);
decompiler.getPluginsContext().setGuiContext(guiPluginsContext);
decompiler.getPluginManager().setGuiContext(guiPluginsContext);
}
public GuiPluginsContext getGuiPluginsContext() {
@@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -28,10 +29,11 @@ import org.slf4j.LoggerFactory;
import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.api.args.UserRenamesMappingsMode;
import jadx.api.JadxDecompiler;
import jadx.core.Jadx;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
@@ -64,7 +66,7 @@ public class DiskCodeCache implements ICodeCache {
codeVersionFile = baseDir.resolve("code-version");
namesMapFile = baseDir.resolve("names-map");
JadxArgs args = root.getArgs();
codeVersion = buildCodeVersion(args);
codeVersion = buildCodeVersion(args, root.getDecompiler());
writePool = Executors.newFixedThreadPool(args.getThreadsCount());
codeMetadataAdapter = new CodeMetadataAdapter(root);
allClsIds = buildClassIdsMap(root.getClasses());
@@ -193,24 +195,28 @@ public class DiskCodeCache implements ICodeCache {
}
}
private String buildCodeVersion(JadxArgs args) {
private String buildCodeVersion(JadxArgs args, @Nullable JadxDecompiler decompiler) {
List<File> inputFiles = new ArrayList<>(args.getInputFiles());
Path userMappingPath = args.getUserRenamesMappingsPath();
if (args.getUserRenamesMappingsMode() != UserRenamesMappingsMode.IGNORE
&& userMappingPath != null
&& Files.exists(userMappingPath)) {
inputFiles.add(userMappingPath.toFile());
}
File generatedMappingFile = args.getGeneratedRenamesMappingFile();
if (args.getGeneratedRenamesMappingFileMode().shouldRead()
&& generatedMappingFile != null
&& generatedMappingFile.exists()) {
inputFiles.add(generatedMappingFile);
&& args.getGeneratedRenamesMappingFile() != null
&& args.getGeneratedRenamesMappingFile().exists()) {
inputFiles.add(args.getGeneratedRenamesMappingFile());
}
return DATA_FORMAT_VERSION
+ ":" + Jadx.getVersion()
+ ":" + args.makeCodeArgsHash()
+ ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath));
+ ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath))
+ ":" + FileUtils.md5Sum(buildPluginsHash(decompiler));
}
private String buildPluginsHash(JadxDecompiler decompiler) {
if (decompiler == null) {
return "";
}
return decompiler.getPluginManager().getResolvedPluginContexts()
.stream()
.map(PluginContext::getInputsHash)
.collect(Collectors.joining());
}
private int getClsId(String clsFullName) {
@@ -7,7 +7,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
@@ -65,6 +67,7 @@ public class JadxProject {
jadxArgs.setInputFiles(FileUtils.toFiles(getFilePaths()));
jadxArgs.setUserRenamesMappingsPath(getMappingsPath());
jadxArgs.setCodeData(getCodeData());
jadxArgs.getPluginOptions().putAll(data.getPluginOptions());
}
public @Nullable Path getWorkingDir() {
@@ -176,6 +179,18 @@ public class JadxProject {
changed();
}
/**
* Do not expose options map directly to be able to intercept changes
*/
public void updatePluginOptions(Consumer<Map<String, String>> update) {
update.accept(data.getPluginOptions());
changed();
}
public @Nullable String getPluginOption(String key) {
return data.getPluginOptions().get(key);
}
public @NotNull Path getCacheDir() {
Path cacheDir = data.getCacheDir();
if (cacheDir != null) {
@@ -39,7 +39,7 @@ public class JadxSettingsStorage {
private static Path initConfigFile() {
ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx");
Path confPath = Paths.get(jadxDirs.configDir, "config.json");
Path confPath = Paths.get(jadxDirs.configDir, "gui.json");
if (!Files.exists(confPath)) {
copyFromPreferences(confPath);
}
@@ -22,8 +22,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
@@ -62,9 +64,10 @@ import jadx.api.JadxArgs;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.args.GeneratedRenamesMappingFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.OptionDescription.OptionFlag;
import jadx.core.plugins.PluginContext;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.usage.UsageCacheMode;
import jadx.gui.ui.MainWindow;
@@ -75,7 +78,6 @@ import jadx.gui.utils.LangLocale;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.plugins.CollectPluginOptions;
import jadx.gui.utils.plugins.PluginWithOptions;
import jadx.gui.utils.ui.DocumentUpdateListener;
public class JadxSettingsWindow extends JDialog {
@@ -613,34 +615,54 @@ public class JadxSettingsWindow extends JDialog {
private SettingsGroup makePluginOptionsGroup() {
SettingsGroup pluginsGroup = new SettingsGroup(NLS.str("preferences.plugins"));
List<PluginWithOptions> list = new CollectPluginOptions(mainWindow.getWrapper()).build();
for (PluginWithOptions data : list) {
addPluginOptions(pluginsGroup, data.getPlugin(), data.getOptions());
List<PluginContext> list = new CollectPluginOptions(mainWindow.getWrapper()).build();
for (PluginContext context : list) {
addPluginOptions(pluginsGroup, context);
}
return pluginsGroup;
}
private void addPluginOptions(SettingsGroup pluginsGroup, JadxPlugin plugin, JadxPluginOptions options) {
String pluginId = plugin.getPluginInfo().getPluginId();
private void addPluginOptions(SettingsGroup pluginsGroup, PluginContext context) {
JadxPluginOptions options = context.getOptions();
if (options == null) {
return;
}
String pluginId = context.getPluginId();
for (OptionDescription opt : options.getOptionsDescriptions()) {
if (opt.getFlags().contains(OptionFlag.HIDE_IN_GUI)) {
continue;
}
String optName = opt.name();
String title;
if (pluginId.equals("jadx-script")) {
title = '[' + opt.name().replace("jadx-script.", "script:") + "] " + opt.description();
title = '[' + optName.replace("jadx-script.", "script:") + "] " + opt.description();
} else {
title = '[' + pluginId + "] " + opt.description();
}
Consumer<String> updateFunc;
String curValue;
if (opt.getFlags().contains(OptionFlag.PER_PROJECT)) {
JadxProject project = mainWindow.getProject();
updateFunc = value -> project.updatePluginOptions(m -> m.put(optName, value));
curValue = project.getPluginOption(optName);
} else {
Map<String, String> optionsMap = settings.getPluginOptions();
updateFunc = value -> optionsMap.put(optName, value);
curValue = optionsMap.get(optName);
}
String value = curValue != null ? curValue : opt.defaultValue();
if (opt.values().isEmpty() || opt.getType() == OptionDescription.OptionType.BOOLEAN) {
try {
pluginsGroup.addRow(title, getPluginOptionEditor(opt));
pluginsGroup.addRow(title, getPluginOptionEditor(opt, value, updateFunc));
} catch (Exception e) {
LOG.error("Failed to add editor for plugin option: {}", opt.name(), e);
LOG.error("Failed to add editor for plugin option: {}", optName, e);
}
} else {
String curValue = settings.getPluginOptions().get(opt.name());
JComboBox<String> combo = new JComboBox<>(opt.values().toArray(new String[0]));
combo.setSelectedItem(curValue != null ? curValue : opt.defaultValue());
combo.setSelectedItem(value);
combo.addActionListener(e -> {
settings.getPluginOptions().put(opt.name(), ((String) combo.getSelectedItem()));
updateFunc.accept((String) combo.getSelectedItem());
needReload();
});
pluginsGroup.addRow(title, combo);
@@ -648,16 +670,13 @@ public class JadxSettingsWindow extends JDialog {
}
}
private JComponent getPluginOptionEditor(OptionDescription opt) {
String curValue = settings.getPluginOptions().get(opt.name());
String value = curValue == null ? opt.defaultValue() : curValue;
private JComponent getPluginOptionEditor(OptionDescription opt, String value, Consumer<String> updateFunc) {
switch (opt.getType()) {
case STRING:
JTextField textField = new JTextField();
textField.setText(value == null ? "" : value);
textField.getDocument().addDocumentListener(new DocumentUpdateListener(event -> {
settings.getPluginOptions().put(opt.name(), textField.getText());
updateFunc.accept(textField.getText());
needReload();
}));
return textField;
@@ -666,7 +685,7 @@ public class JadxSettingsWindow extends JDialog {
JSpinner numberField = new JSpinner();
numberField.setValue(safeStringToInt(value, 0));
numberField.addChangeListener(e -> {
settings.getPluginOptions().put(opt.name(), numberField.getValue().toString());
updateFunc.accept(numberField.getValue().toString());
needReload();
});
return numberField;
@@ -676,7 +695,7 @@ public class JadxSettingsWindow extends JDialog {
boolField.setSelected(Objects.equals(value, "yes") || Objects.equals(value, "true"));
boolField.addItemListener(e -> {
boolean editorValue = e.getStateChange() == ItemEvent.SELECTED;
settings.getPluginOptions().put(opt.name(), editorValue ? "yes" : "no");
updateFunc.accept(editorValue ? "yes" : "no");
needReload();
});
return boolField;
@@ -8,6 +8,7 @@ import jadx.api.JavaClass;
import jadx.gui.settings.data.TabViewState;
import jadx.gui.settings.data.ViewPoint;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JInputMapping;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
@@ -62,6 +63,9 @@ public class TabStateViewAdapter {
return mw.getTreeRoot()
.followStaticPath("JInputs", "JInputScripts")
.searchNode(node -> node instanceof JInputScript && node.getName().equals(tvs.getTabPath()));
case "mapping":
return mw.getTreeRoot().followStaticPath("JInputs", "JInputMapping");
}
return null;
}
@@ -82,6 +86,10 @@ public class TabStateViewAdapter {
tvs.setTabPath(node.getName());
return true;
}
if (node instanceof JInputMapping) {
tvs.setType("mapping");
return true;
}
return false;
}
}
@@ -3,7 +3,9 @@ package jadx.gui.settings.data;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
@@ -22,6 +24,7 @@ public class ProjectData {
private @Nullable Path cacheDir;
private boolean enableLiveReload = false;
private List<String> searchHistory = new ArrayList<>();
protected Map<String, String> pluginOptions = new HashMap<>();
public List<Path> getFiles() {
return files;
@@ -122,4 +125,8 @@ public class ProjectData {
public void setSearchHistory(List<String> searchHistory) {
this.searchHistory = searchHistory;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
}
@@ -145,6 +145,7 @@ import jadx.gui.utils.UiUtils;
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
import jadx.gui.utils.ui.ActionHandler;
import jadx.gui.utils.ui.NodeLabel;
import jadx.plugins.mappings.RenameMappingsOptions;
import jadx.plugins.mappings.save.MappingExporter;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
@@ -389,7 +390,7 @@ public class MainWindow extends JFrame {
update();
}
private void openMappings(MappingFormat mappingFormat) {
private void openMappings(MappingFormat mappingFormat, boolean inverted) {
FileDialogWrapper fileDialog = new FileDialogWrapper(this, FileOpenMode.CUSTOM_OPEN);
fileDialog.setTitle(NLS.str("file.open_mappings"));
if (mappingFormat.hasSingleFile()) {
@@ -407,12 +408,17 @@ public class MainWindow extends JFrame {
LOG.info("Loading mappings from: {}", filePath.toAbsolutePath());
project.setMappingsPath(filePath);
currentMappingFormat = mappingFormat;
project.updatePluginOptions(options -> {
options.put(RenameMappingsOptions.FORMAT_OPT, mappingFormat.name());
options.put(RenameMappingsOptions.INVERT_OPT, inverted ? "yes" : "no");
});
reopen();
}
public void closeMappingsAndRemoveFromProject() {
project.setMappingsPath(null);
currentMappingFormat = null;
reopen();
}
private void saveMappings() {
@@ -1012,88 +1018,25 @@ public class MainWindow extends JFrame {
liveReloadMenuItem = new JCheckBoxMenuItem(liveReload);
liveReloadMenuItem.setState(project.isEnableLiveReload());
ActionHandler openProGuardMappings = new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD));
openProGuardMappings.setNameAndDesc("Proguard");
Action openTiny2Mappings = new AbstractAction("Tiny v2 file") {
@Override
public void actionPerformed(ActionEvent e) {
openMappings(MappingFormat.TINY_2);
}
};
openTiny2Mappings.putValue(Action.SHORT_DESCRIPTION, "Tiny v2 file");
Action openEnigmaMappings = new AbstractAction("Enigma file") {
@Override
public void actionPerformed(ActionEvent e) {
openMappings(MappingFormat.ENIGMA);
}
};
openEnigmaMappings.putValue(Action.SHORT_DESCRIPTION, "Enigma file");
Action openEnigmaDirMappings = new AbstractAction("Enigma directory") {
@Override
public void actionPerformed(ActionEvent e) {
openMappings(MappingFormat.ENIGMA_DIR);
}
};
openEnigmaDirMappings.putValue(Action.SHORT_DESCRIPTION, "Enigma directory");
openMappingsMenu = new JMenu(NLS.str("file.open_mappings"));
openMappingsMenu.add(openProGuardMappings);
openMappingsMenu.add(openTiny2Mappings);
openMappingsMenu.add(openEnigmaMappings);
openMappingsMenu.add(openEnigmaDirMappings);
openMappingsMenu.add(new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD, true)).withNameAndDesc("Proguard (inverted)"));
openMappingsMenu.add(new ActionHandler(ev -> openMappings(MappingFormat.PROGUARD, false)).withNameAndDesc("Proguard"));
saveMappingsAction = new AbstractAction(NLS.str("file.save_mappings")) {
@Override
public void actionPerformed(ActionEvent e) {
saveMappings();
}
};
saveMappingsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_mappings"));
ActionHandler saveProGuardMappings = new ActionHandler(ev -> saveMappingsAs(MappingFormat.PROGUARD));
saveProGuardMappings.setNameAndDesc("Proguard");
Action saveMappingsAsTiny2 = new AbstractAction("Tiny v2 file") {
@Override
public void actionPerformed(ActionEvent e) {
saveMappingsAs(MappingFormat.TINY_2);
}
};
saveMappingsAsTiny2.putValue(Action.SHORT_DESCRIPTION, "Tiny v2 file");
Action saveMappingsAsEnigma = new AbstractAction("Enigma file") {
@Override
public void actionPerformed(ActionEvent e) {
saveMappingsAs(MappingFormat.ENIGMA);
}
};
saveMappingsAsEnigma.putValue(Action.SHORT_DESCRIPTION, "Enigma file");
Action saveMappingsAsEnigmaDir = new AbstractAction("Enigma directory") {
@Override
public void actionPerformed(ActionEvent e) {
saveMappingsAs(MappingFormat.ENIGMA_DIR);
}
};
saveMappingsAsEnigmaDir.putValue(Action.SHORT_DESCRIPTION, "Enigma directory");
saveMappingsAction = new ActionHandler(this::saveMappings).withNameAndDesc(NLS.str("file.save_mappings"));
saveMappingsAsMenu = new JMenu(NLS.str("file.save_mappings_as"));
saveMappingsAsMenu.add(saveProGuardMappings);
saveMappingsAsMenu.add(saveMappingsAsTiny2);
saveMappingsAsMenu.add(saveMappingsAsEnigma);
saveMappingsAsMenu.add(saveMappingsAsEnigmaDir);
closeMappingsAction = new AbstractAction(NLS.str("file.close_mappings")) {
@Override
public void actionPerformed(ActionEvent e) {
closeMappingsAndRemoveFromProject();
reopen();
for (MappingFormat mappingFormat : MappingFormat.values()) {
if (mappingFormat != MappingFormat.PROGUARD) {
openMappingsMenu.add(new ActionHandler(ev -> openMappings(mappingFormat, false))
.withNameAndDesc(mappingFormat.name));
}
};
closeMappingsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.close_mappings"));
saveMappingsAsMenu.add(new ActionHandler(ev -> saveMappingsAs(mappingFormat))
.withNameAndDesc(mappingFormat.name));
}
closeMappingsAction = new ActionHandler(ev -> closeMappingsAndRemoveFromProject())
.withNameAndDesc(NLS.str("file.close_mappings"));
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), Icons.SAVE_ALL) {
@Override
@@ -1,16 +1,14 @@
package jadx.gui.utils.plugins;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginManager;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.plugins.PluginContext;
import jadx.gui.JadxWrapper;
/**
@@ -21,47 +19,32 @@ import jadx.gui.JadxWrapper;
public class CollectPluginOptions {
private final JadxWrapper wrapper;
private final Map<Class<?>, PluginWithOptions> plugins;
public CollectPluginOptions(JadxWrapper wrapper) {
this.wrapper = wrapper;
this.plugins = new HashMap<>();
}
public List<PluginWithOptions> build() {
wrapper.getCurrentDecompiler().ifPresent(decompiler -> {
List<JadxPlugin> loadedPlugins = decompiler.getPluginManager().getResolvedPlugins();
addOptions(decompiler, loadedPlugins);
});
public List<PluginContext> build() {
SortedSet<PluginContext> allPlugins = new TreeSet<>();
wrapper.getCurrentDecompiler()
.ifPresent(decompiler -> allPlugins.addAll(decompiler.getPluginManager().getResolvedPluginContexts()));
// collect and init not loaded plugins in new context
try (JadxDecompiler decompiler = new JadxDecompiler(new JadxArgs())) {
JadxPluginManager pluginManager = decompiler.getPluginManager();
List<JadxPlugin> missingPlugins = new ArrayList<>();
for (JadxPlugin plugin : pluginManager.getAllPlugins()) {
if (!plugins.containsKey(plugin.getClass())) {
missingPlugins.add(plugin);
pluginManager.load();
SortedSet<PluginContext> missingPlugins = new TreeSet<>();
for (PluginContext context : pluginManager.getAllPluginContexts()) {
if (!allPlugins.contains(context)) {
missingPlugins.add(context);
}
}
pluginManager.init(decompiler.getPluginsContext(), missingPlugins);
addOptions(decompiler, missingPlugins);
pluginManager.init(missingPlugins);
allPlugins.addAll(missingPlugins);
}
return plugins.values().stream()
.filter(data -> data != PluginWithOptions.NULL)
return allPlugins.stream()
.filter(context -> context.getOptions() != null)
.sorted()
.collect(Collectors.toList());
}
private void addOptions(JadxDecompiler decompiler, List<JadxPlugin> loadedPlugins) {
Map<JadxPlugin, JadxPluginOptions> optionsMap = decompiler.getPluginsContext().getOptionsMap();
for (JadxPlugin loadedPlugin : loadedPlugins) {
JadxPluginOptions pluginOptions = optionsMap.get(loadedPlugin);
PluginWithOptions options;
if (pluginOptions != null) {
options = new PluginWithOptions(loadedPlugin, pluginOptions);
} else {
options = PluginWithOptions.NULL;
}
plugins.put(loadedPlugin.getClass(), options);
}
}
}
@@ -28,6 +28,11 @@ public class ActionHandler extends AbstractAction {
putValue(NAME, name);
}
public ActionHandler withNameAndDesc(String name) {
setNameAndDesc(name);
return this;
}
public void setNameAndDesc(String name) {
setName(name);
setShortDescription(name);